@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
package/README.md CHANGED
@@ -148,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
148
148
  ```
149
149
 
150
150
  The installer resolves the latest stable release tag and installs that version by default.
151
- To pin a version: `SWARMCLAW_VERSION=v0.7.7 curl ... | bash`
151
+ To pin a version: `SWARMCLAW_VERSION=v0.8.0 curl ... | bash`
152
152
 
153
153
  Or run locally from the repo (friendly for non-technical users):
154
154
 
@@ -464,7 +464,7 @@ Policy is enforced in both chat tool construction and direct forced tool invocat
464
464
 
465
465
  Configure these in **Settings**:
466
466
 
467
- - **Voice** — set `ElevenLabs API Key`, `ElevenLabs Voice ID`, and `Speech Recognition Language`
467
+ - **Voice** — set `ElevenLabs API Key`, `ElevenLabs Voice ID`, and `Speech Recognition Language`. Outbound voice-note sends retry with the built-in fallback ElevenLabs voice when a saved default voice is rejected as paid-only.
468
468
  - **Heartbeat** — set `Heartbeat Interval (Seconds)` and `Heartbeat Prompt` for ongoing chat pings
469
469
  - **Global heartbeat safety** — use `Stop All Heartbeats` to disable heartbeat across all chats and cancel in-flight heartbeat runs.
470
470
 
@@ -529,7 +529,7 @@ Built-in capabilities now resolve to a single canonical plugin family ID across
529
529
 
530
530
  Agents in SwarmClaw are "aware" of the plugin system. If an agent lacks a tool needed for a task, it can:
531
531
  1. **Discover**: Scan the system for all installed plugins.
532
- 2. **Search Marketplace**: Autonomously search **ClawHub** and the **SwarmClaw Registry** for new capabilities.
532
+ 2. **Search Marketplace**: Autonomously search **ClawHub** and the **SwarmForge-backed first-party marketplace** for new capabilities.
533
533
  3. **Request Access**: Prompt the user in-chat to enable a specific installed plugin.
534
534
  4. **Install Request**: Suggest installing a new plugin from a marketplace URL to fill a capability gap (requires user approval).
535
535
 
@@ -663,8 +663,8 @@ npm run dev:webpack # Fallback to webpack dev server (if Turbopack crashes)
663
663
  npm run dev:clean # Clear .next cache then restart dev server
664
664
  npm run build # Production build
665
665
  npm run build:ci # CI build (skips ESLint; lint baseline runs separately)
666
- npm run start # Start production server
667
- npm run start:standalone # Start standalone server after build
666
+ npm run start # Start the standalone production server after build
667
+ npm run start:standalone # Alias for the standalone production server
668
668
  npm run lint # ESLint
669
669
  npm run lint:baseline # Fail only on net-new lint issues vs .eslint-baseline.json
670
670
  npm run lint:baseline:update # Refresh lint baseline intentionally
@@ -701,8 +701,8 @@ npm run update:easy # safe update helper for local installs
701
701
  SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
702
702
 
703
703
  ```bash
704
- # example patch release (v0.7.7 style)
705
- npm version patch
704
+ # example minor release (v0.8.0 style)
705
+ npm version minor
706
706
  git push origin main --follow-tags
707
707
  ```
708
708
 
@@ -711,15 +711,13 @@ On `v*` tags, GitHub Actions will:
711
711
  2. Create a GitHub Release
712
712
  3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
713
713
 
714
- #### v0.7.7 Release Readiness Notes
714
+ #### v0.8.0 Release Readiness Notes
715
715
 
716
- Before shipping `v0.7.7`, confirm the following user-facing changes are reflected in docs:
716
+ Before shipping `v0.8.0`, confirm the following user-facing changes are reflected in docs:
717
717
 
718
- 1. OpenClaw docs cover Smart Deploy end-to-end: onboarding, Providers, gateway editor, official-only SSH/VPS flows, safe exposure presets, and restore/backup lifecycle controls.
719
- 2. Agent and provider docs explain gateway routing by tags/use-case, richer external runtime visibility, and import/export/clone flows for saved gateways.
720
- 3. Site and README install/version strings are updated to `v0.7.7`, including install snippets, release notes index text, and sidebar/footer labels.
721
- 4. Release notes summarize the user-visible operator changes from the current worktree, especially SSH deploy, remote lifecycle controls, routing preferences, and onboarding persistence.
722
- 5. CLI docs include the expanded `openclaw deploy-*`, `openclaw remote-*`, and verify surfaces and do not reference removed or unofficial deployment paths.
718
+ 1. Voice and connector docs mention that outbound voice-note sends now retry with the built-in ElevenLabs fallback voice when a configured default voice is rejected as paid-only.
719
+ 2. Release notes call out the Molly-style regression case explicitly: WhatsApp voice-note delivery should keep working even if the saved default voice ID points at a paid library voice.
720
+ 3. Site and README install/version strings are updated to `v0.8.0`, including install snippets, release notes index text, and sidebar/footer labels.
723
721
 
724
722
  ## CLI
725
723
 
package/next.config.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import type { NextConfig } from "next";
2
2
  import { execSync } from "child_process";
3
3
  import { networkInterfaces } from "os";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
4
6
  import { DIRECT_NAV_SEGMENTS } from "./view-route-paths";
5
7
 
8
+ const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url))
9
+
6
10
  function getGitSha(): string {
7
11
  try {
8
12
  return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim()
@@ -40,10 +44,17 @@ function getAllowedDevOrigins(): string[] {
40
44
 
41
45
  const nextConfig: NextConfig = {
42
46
  output: 'standalone',
47
+ outputFileTracingExcludes: {
48
+ '/api/**': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
49
+ instrumentation: ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
50
+ '/instrumentation': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
51
+ 'next-server': ['data/browser-profiles/**/*', 'data/browser-profiles-regression/**/*'],
52
+ },
43
53
  turbopack: {
44
54
  // Pin workspace root to the project directory so a stale lockfile
45
- // in a parent folder (e.g. ~/) doesn't confuse native module resolution.
46
- root: process.cwd(),
55
+ // in a parent folder (e.g. ~/) or a nested launch cwd doesn't confuse
56
+ // native module resolution.
57
+ root: PROJECT_ROOT,
47
58
  },
48
59
  experimental: {
49
60
  // Disable Turbopack persistent cache — concurrent HMR writes cause
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.7.7",
3
+ "version": "0.8.0",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -47,8 +47,9 @@
47
47
  "dev:clean": "rm -rf .next && next dev --hostname 0.0.0.0 -p 3456",
48
48
  "build": "next build",
49
49
  "build:ci": "NEXT_DISABLE_ESLINT=1 next build",
50
- "start": "next start",
50
+ "start": "node .next/standalone/server.js",
51
51
  "start:standalone": "node .next/standalone/server.js",
52
+ "smoke:browser": "node ./scripts/browser-route-smoke.mjs",
52
53
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
53
54
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
54
55
  "lint": "eslint",
@@ -81,6 +82,7 @@
81
82
  "cron-parser": "^5.5.0",
82
83
  "cronstrue": "^3.12.0",
83
84
  "discord.js": "^14.25.1",
85
+ "ethers": "^6.16.0",
84
86
  "exceljs": "^4.4.0",
85
87
  "grammy": "^1.40.0",
86
88
  "highlight.js": "^11.11.1",
@@ -1,12 +1,21 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
2
3
  import { ensureAgentThreadSession } from '@/lib/server/agent-thread-session'
4
+ import { loadAgents } from '@/lib/server/storage'
3
5
 
4
6
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
5
7
  const { id: agentId } = await params
6
8
  const body = await req.json().catch(() => ({}))
7
9
  const user = body.user || 'default'
10
+ const agent = loadAgents()[agentId]
11
+ if (!agent) {
12
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
13
+ }
8
14
  const session = ensureAgentThreadSession(agentId, user)
9
15
  if (!session) {
16
+ if (isAgentDisabled(agent)) {
17
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start new chats') }, { status: 409 })
18
+ }
10
19
  return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
11
20
  }
12
21
  return NextResponse.json(session)
@@ -3,6 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadAgents, loadSessions, loadUsage, saveAgents, logActivity } from '@/lib/server/storage'
4
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
+ import { ensureDaemonStarted } from '@/lib/server/daemon-state'
6
7
  import { getAgentSpendWindows } from '@/lib/server/cost'
7
8
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
8
9
  import { z } from 'zod'
@@ -10,6 +11,7 @@ export const dynamic = 'force-dynamic'
10
11
 
11
12
 
12
13
  export async function GET(req: Request) {
14
+ ensureDaemonStarted('api/agents:get')
13
15
  const agents = loadAgents()
14
16
  const sessions = loadSessions()
15
17
  const usage = loadUsage()
@@ -42,6 +44,7 @@ export async function GET(req: Request) {
42
44
  }
43
45
 
44
46
  export async function POST(req: Request) {
47
+ ensureDaemonStarted('api/agents:post')
45
48
  const raw = await req.json()
46
49
  const parsed = AgentCreateSchema.safeParse(raw)
47
50
  if (!parsed.success) {
@@ -82,6 +85,7 @@ export async function POST(req: Request) {
82
85
  capabilities: body.capabilities,
83
86
  thinkingLevel: body.thinkingLevel || undefined,
84
87
  autoRecovery: body.autoRecovery || false,
88
+ disabled: body.disabled || false,
85
89
  heartbeatEnabled: body.heartbeatEnabled || false,
86
90
  heartbeatInterval: body.heartbeatInterval,
87
91
  heartbeatIntervalSec: body.heartbeatIntervalSec,
@@ -0,0 +1,133 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { POST as createAgentThread } from './[id]/thread/route'
5
+ import { loadAgents, loadSessions, saveAgents, saveSessions } from '@/lib/server/storage'
6
+
7
+ const originalAgents = loadAgents()
8
+ const originalSessions = loadSessions()
9
+
10
+ function routeParams(id: string) {
11
+ return { params: Promise.resolve({ id }) }
12
+ }
13
+
14
+ function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
15
+ const agents = loadAgents()
16
+ const now = Date.now()
17
+ agents[id] = {
18
+ id,
19
+ name: 'Thread Test Agent',
20
+ description: 'Agent thread route smoke',
21
+ systemPrompt: 'Be helpful.',
22
+ provider: 'openai',
23
+ model: 'gpt-4o-mini',
24
+ credentialId: null,
25
+ fallbackCredentialIds: [],
26
+ apiEndpoint: null,
27
+ gatewayProfileId: null,
28
+ plugins: ['memory'],
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ ...overrides,
32
+ }
33
+ saveAgents(agents)
34
+ }
35
+
36
+ afterEach(() => {
37
+ saveAgents(originalAgents)
38
+ saveSessions(originalSessions)
39
+ })
40
+
41
+ test('POST /api/agents/[id]/thread returns 409 for a disabled agent without an existing thread', async () => {
42
+ seedAgent('agent-thread-disabled', { disabled: true })
43
+
44
+ const response = await createAgentThread(new Request('http://local/api/agents/agent-thread-disabled/thread', {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify({ user: 'default' }),
48
+ }), routeParams('agent-thread-disabled'))
49
+
50
+ assert.equal(response.status, 409)
51
+ const payload = await response.json() as Record<string, unknown>
52
+ assert.match(String(payload.error || ''), /disabled/i)
53
+ })
54
+
55
+ test('POST /api/agents/[id]/thread reuses an existing thread for a disabled agent', async () => {
56
+ seedAgent('agent-thread-disabled-existing', {
57
+ disabled: true,
58
+ threadSessionId: 'session-disabled-existing',
59
+ })
60
+
61
+ const sessions = loadSessions()
62
+ sessions['session-disabled-existing'] = {
63
+ id: 'session-disabled-existing',
64
+ name: 'Thread Test Agent',
65
+ shortcutForAgentId: 'agent-thread-disabled-existing',
66
+ cwd: '/tmp',
67
+ user: 'default',
68
+ provider: 'openai',
69
+ model: 'gpt-4o-mini',
70
+ credentialId: null,
71
+ fallbackCredentialIds: [],
72
+ apiEndpoint: null,
73
+ gatewayProfileId: null,
74
+ routePreferredGatewayTags: [],
75
+ routePreferredGatewayUseCase: null,
76
+ claudeSessionId: null,
77
+ codexThreadId: null,
78
+ opencodeSessionId: null,
79
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
80
+ messages: [],
81
+ createdAt: 1,
82
+ lastActiveAt: 1,
83
+ active: false,
84
+ sessionType: 'human',
85
+ agentId: 'agent-thread-disabled-existing',
86
+ parentSessionId: null,
87
+ plugins: ['memory'],
88
+ tools: ['memory'],
89
+ heartbeatEnabled: false,
90
+ heartbeatIntervalSec: null,
91
+ heartbeatTarget: null,
92
+ sessionResetMode: null,
93
+ sessionIdleTimeoutSec: null,
94
+ sessionMaxAgeSec: null,
95
+ sessionDailyResetAt: null,
96
+ sessionResetTimezone: null,
97
+ thinkingLevel: null,
98
+ browserProfileId: null,
99
+ connectorThinkLevel: null,
100
+ connectorSessionScope: null,
101
+ connectorReplyMode: null,
102
+ connectorThreadBinding: null,
103
+ connectorGroupPolicy: null,
104
+ connectorIdleTimeoutSec: null,
105
+ connectorMaxAgeSec: null,
106
+ mailbox: null,
107
+ connectorContext: null,
108
+ lastAutoMemoryAt: null,
109
+ lastHeartbeatText: null,
110
+ lastHeartbeatSentAt: null,
111
+ lastSessionResetAt: null,
112
+ lastSessionResetReason: null,
113
+ identityState: null,
114
+ sessionArchiveState: null,
115
+ pinned: false,
116
+ file: null,
117
+ queuedCount: 0,
118
+ currentRunId: null,
119
+ canvasContent: null,
120
+ }
121
+ saveSessions(sessions)
122
+
123
+ const response = await createAgentThread(new Request('http://local/api/agents/agent-thread-disabled-existing/thread', {
124
+ method: 'POST',
125
+ headers: { 'content-type': 'application/json' },
126
+ body: JSON.stringify({ user: 'default' }),
127
+ }), routeParams('agent-thread-disabled-existing'))
128
+
129
+ assert.equal(response.status, 200)
130
+ const payload = await response.json() as Record<string, unknown>
131
+ assert.equal(payload.id, 'session-disabled-existing')
132
+ assert.equal(payload.agentId, 'agent-thread-disabled-existing')
133
+ })
@@ -0,0 +1,148 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-approvals-route-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ test('GET and POST /api/approvals smoke the pending approval flow end-to-end', () => {
36
+ const output = runWithTempDataDir(`
37
+ const storageMod = await import('./src/lib/server/storage.ts')
38
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
39
+ const routeMod = await import('./src/app/api/approvals/route.ts')
40
+ const storage = storageMod.default || storageMod
41
+ const approvals = approvalsMod.default || approvalsMod
42
+ const route = routeMod.default || routeMod
43
+
44
+ const now = Date.now()
45
+ storage.saveSettings({
46
+ approvalsEnabled: true,
47
+ approvalAutoApproveCategories: [],
48
+ })
49
+
50
+ const approval = await approvals.requestApprovalMaybeAutoApprove({
51
+ category: 'tool_access',
52
+ title: 'Enable Plugin: shell',
53
+ description: 'Need shell access for a smoke test.',
54
+ data: { toolId: 'shell', pluginId: 'shell' },
55
+ })
56
+
57
+ const pendingBefore = await route.GET()
58
+ const pendingBeforeJson = await pendingBefore.json()
59
+
60
+ const approveResponse = await route.POST(new Request('http://local/api/approvals', {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify({ id: approval.id, approved: true }),
64
+ }))
65
+ const approvePayload = await approveResponse.json()
66
+
67
+ const pendingAfter = await route.GET()
68
+ const pendingAfterJson = await pendingAfter.json()
69
+
70
+ const storedApproval = storage.loadApprovals()[approval.id]
71
+ console.log(JSON.stringify({
72
+ pendingBeforeCount: Array.isArray(pendingBeforeJson) ? pendingBeforeJson.length : -1,
73
+ pendingBeforeId: Array.isArray(pendingBeforeJson) ? pendingBeforeJson[0]?.id || null : null,
74
+ approveStatus: approveResponse.status,
75
+ approvePayload,
76
+ pendingAfterCount: Array.isArray(pendingAfterJson) ? pendingAfterJson.length : -1,
77
+ storedStatus: storedApproval?.status || null,
78
+ }))
79
+ `)
80
+
81
+ assert.equal(output.pendingBeforeCount, 1)
82
+ assert.notEqual(output.pendingBeforeId, null)
83
+ assert.equal(output.approveStatus, 200)
84
+ assert.equal(output.approvePayload?.ok, true)
85
+ assert.equal(output.pendingAfterCount, 0)
86
+ assert.equal(output.storedStatus, 'approved')
87
+ })
88
+
89
+ test('POST /api/approvals rejects invalid payloads and remains idempotent for repeated decisions', () => {
90
+ const output = runWithTempDataDir(`
91
+ const storageMod = await import('./src/lib/server/storage.ts')
92
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
93
+ const routeMod = await import('./src/app/api/approvals/route.ts')
94
+ const storage = storageMod.default || storageMod
95
+ const approvals = approvalsMod.default || approvalsMod
96
+ const route = routeMod.default || routeMod
97
+
98
+ const now = Date.now()
99
+ storage.saveSettings({
100
+ approvalsEnabled: true,
101
+ approvalAutoApproveCategories: [],
102
+ })
103
+
104
+ const approval = await approvals.requestApprovalMaybeAutoApprove({
105
+ category: 'tool_access',
106
+ title: 'Enable Plugin: shell',
107
+ description: 'Need shell access for idempotency test.',
108
+ data: { toolId: 'shell', pluginId: 'shell' },
109
+ })
110
+
111
+ const invalidResponse = await route.POST(new Request('http://local/api/approvals', {
112
+ method: 'POST',
113
+ headers: { 'content-type': 'application/json' },
114
+ body: JSON.stringify({ id: approval.id }),
115
+ }))
116
+ const invalidPayload = await invalidResponse.json()
117
+
118
+ const firstApprove = await route.POST(new Request('http://local/api/approvals', {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'application/json' },
121
+ body: JSON.stringify({ id: approval.id, approved: true }),
122
+ }))
123
+ const secondApprove = await route.POST(new Request('http://local/api/approvals', {
124
+ method: 'POST',
125
+ headers: { 'content-type': 'application/json' },
126
+ body: JSON.stringify({ id: approval.id, approved: true }),
127
+ }))
128
+
129
+ const pending = await route.GET()
130
+ const pendingJson = await pending.json()
131
+ const storedApproval = storage.loadApprovals()[approval.id]
132
+ console.log(JSON.stringify({
133
+ invalidStatus: invalidResponse.status,
134
+ invalidError: invalidPayload?.error || null,
135
+ firstApproveStatus: firstApprove.status,
136
+ secondApproveStatus: secondApprove.status,
137
+ pendingCount: Array.isArray(pendingJson) ? pendingJson.length : -1,
138
+ storedStatus: storedApproval?.status || null,
139
+ }))
140
+ `)
141
+
142
+ assert.equal(output.invalidStatus, 400)
143
+ assert.match(String(output.invalidError || ''), /id and approved required/i)
144
+ assert.equal(output.firstApproveStatus, 200)
145
+ assert.equal(output.secondApproveStatus, 200)
146
+ assert.equal(output.pendingCount, 0)
147
+ assert.equal(output.storedStatus, 'approved')
148
+ })
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
+ import { normalizeCanvasContent } from '@/lib/canvas-content'
4
5
 
5
6
  export async function GET(_req: Request, { params }: { params: Promise<{ sessionId: string }> }) {
6
7
  const { sessionId } = await params
@@ -21,7 +22,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ session
21
22
  const session = sessions[sessionId]
22
23
  if (!session) return NextResponse.json({ error: 'Session not found' }, { status: 404 })
23
24
 
24
- ;(session as Record<string, unknown>).canvasContent = body.content || null
25
+ const nextContent = normalizeCanvasContent(body.document ?? body.content)
26
+ ;(session as Record<string, unknown>).canvasContent = nextContent
25
27
  session.lastActiveAt = Date.now()
26
28
  sessions[sessionId] = session
27
29
  saveSessions(sessions)
@@ -23,6 +23,7 @@ import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
23
23
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
24
24
  import { applyAgentReactionsFromText } from '@/lib/server/chatroom-orchestration'
25
25
  import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
26
+ import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/assistant-control'
26
27
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
27
28
 
28
29
  export const dynamic = 'force-dynamic'
@@ -228,7 +229,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
228
229
  history,
229
230
  })
230
231
 
231
- const responseText = result.finalResponse || result.fullText || fullText
232
+ const rawResponseText = result.finalResponse || result.fullText || fullText
233
+ const responseText = stripHiddenControlTokens(rawResponseText)
232
234
 
233
235
  // Don't persist empty or error-only messages — they pollute chat history
234
236
  if (!responseText.trim() && agentError) {
@@ -238,7 +240,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
238
240
  return []
239
241
  }
240
242
 
241
- if (responseText.trim()) {
243
+ if (responseText.trim() && !shouldSuppressHiddenControlText(rawResponseText)) {
242
244
  appendSyntheticSessionMessage(syntheticSession.id, 'assistant', responseText)
243
245
  const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
244
246
  const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
@@ -2,6 +2,27 @@ import { NextResponse } from 'next/server'
2
2
  import { spawn } from 'child_process'
3
3
  import { loadSessions, devServers, localIP } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
6
+ import net from 'net'
7
+
8
+ function findFreePort(): Promise<number> {
9
+ return new Promise((resolve, reject) => {
10
+ const server = net.createServer()
11
+ server.listen(0, '0.0.0.0', () => {
12
+ const address = server.address()
13
+ const port = typeof address === 'object' && address ? address.port : 0
14
+ server.close((err) => err ? reject(err) : resolve(port))
15
+ })
16
+ server.on('error', reject)
17
+ })
18
+ }
19
+
20
+ function buildDevArgs(framework: string, port: number): string[] {
21
+ if (framework === 'next') {
22
+ return ['--', '--hostname', '0.0.0.0', '--port', String(port)]
23
+ }
24
+ return ['--', '--host', '0.0.0.0', '--port', String(port)]
25
+ }
5
26
 
6
27
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
28
  const { id } = await params
@@ -17,10 +38,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
17
38
  return NextResponse.json({ running: true, url: ds.url })
18
39
  }
19
40
 
20
- const proc = spawn('npm', ['run', 'dev', '--', '--host', '0.0.0.0'], {
21
- cwd: session.cwd,
41
+ const launch = resolveDevServerLaunchDir(session.cwd)
42
+ const port = await findFreePort()
43
+ const proc = spawn('npm', ['run', 'dev', ...buildDevArgs(launch.framework, port)], {
44
+ cwd: launch.launchDir,
22
45
  stdio: ['ignore', 'pipe', 'pipe'],
23
- env: { ...process.env, FORCE_COLOR: '0' },
46
+ env: { ...process.env, FORCE_COLOR: '0', PORT: String(port) },
24
47
  })
25
48
 
26
49
  let output = ''
@@ -45,19 +68,37 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
45
68
  proc.on('close', () => { devServers.delete(id); console.log(`[${id}] dev server stopped`) })
46
69
  proc.on('error', () => devServers.delete(id))
47
70
 
48
- devServers.set(id, { proc, url: `http://${localIP()}:4321` })
49
- console.log(`[${id}] starting dev server in ${session.cwd}`)
71
+ devServers.set(id, { proc, url: `http://${localIP()}:${port}` })
72
+ console.log(`[${id}] starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
50
73
 
51
74
  // Wait for URL detection
52
75
  await new Promise(resolve => setTimeout(resolve, 4000))
53
76
  const ds = devServers.get(id)
54
- return NextResponse.json({ running: !!ds, url: ds?.url || `http://${localIP()}:4321` })
77
+ if (!ds) {
78
+ return NextResponse.json({
79
+ running: false,
80
+ error: 'Dev server exited during startup',
81
+ cwd: launch.launchDir,
82
+ sessionCwd: session.cwd,
83
+ framework: launch.framework,
84
+ output: output.slice(-4000),
85
+ }, { status: 502 })
86
+ }
87
+ return NextResponse.json({
88
+ running: true,
89
+ url: ds.url,
90
+ cwd: launch.launchDir,
91
+ sessionCwd: session.cwd,
92
+ framework: launch.framework,
93
+ })
55
94
 
56
95
  } else if (action === 'stop') {
57
96
  if (devServers.has(id)) {
58
97
  const ds = devServers.get(id)!
59
98
  try { ds.proc.kill('SIGTERM') } catch {}
60
- try { process.kill(-ds.proc.pid, 'SIGTERM') } catch {}
99
+ if (typeof ds.proc.pid === 'number') {
100
+ try { process.kill(-ds.proc.pid, 'SIGTERM') } catch {}
101
+ }
61
102
  devServers.delete(id)
62
103
  }
63
104
  return NextResponse.json({ running: false })