@swarmclawai/swarmclaw 1.1.4 → 1.1.6

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 (199) hide show
  1. package/README.md +25 -0
  2. package/bin/install-root.js +3 -0
  3. package/bin/server-cmd.js +3 -1
  4. package/bin/update-cmd.js +4 -1
  5. package/package.json +2 -1
  6. package/scripts/easy-setup.mjs +5 -2
  7. package/src/app/api/activity/route.ts +2 -0
  8. package/src/app/api/agents/[id]/route.ts +4 -3
  9. package/src/app/api/agents/bulk/route.ts +55 -0
  10. package/src/app/api/agents/route.ts +3 -1
  11. package/src/app/api/agents/trash/route.ts +5 -2
  12. package/src/app/api/auth/route.ts +14 -1
  13. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +43 -4
  15. package/src/app/api/chatrooms/[id]/members/route.ts +7 -4
  16. package/src/app/api/chatrooms/[id]/moderate/route.ts +3 -1
  17. package/src/app/api/chatrooms/[id]/pins/route.ts +4 -2
  18. package/src/app/api/chatrooms/[id]/reactions/route.ts +6 -4
  19. package/src/app/api/chatrooms/[id]/route.ts +8 -5
  20. package/src/app/api/chatrooms/route.ts +15 -3
  21. package/src/app/api/chats/[id]/deploy/route.ts +3 -1
  22. package/src/app/api/chats/[id]/devserver/route.ts +4 -1
  23. package/src/app/api/chats/[id]/messages/route.ts +8 -4
  24. package/src/app/api/chats/[id]/route.ts +4 -2
  25. package/src/app/api/clawhub/install/route.ts +3 -1
  26. package/src/app/api/connectors/[id]/access/route.ts +3 -1
  27. package/src/app/api/connectors/[id]/route.ts +11 -9
  28. package/src/app/api/connectors/route.ts +3 -1
  29. package/src/app/api/credentials/route.ts +4 -1
  30. package/src/app/api/dashboard/route.ts +130 -0
  31. package/src/app/api/documents/[id]/revisions/route.ts +22 -0
  32. package/src/app/api/documents/[id]/route.ts +17 -1
  33. package/src/app/api/extensions/dependencies/route.ts +3 -1
  34. package/src/app/api/extensions/install/route.ts +5 -1
  35. package/src/app/api/extensions/route.ts +7 -2
  36. package/src/app/api/files/open/route.ts +4 -1
  37. package/src/app/api/mcp-servers/[id]/route.ts +3 -1
  38. package/src/app/api/mcp-servers/route.ts +3 -1
  39. package/src/app/api/notifications/route.ts +3 -1
  40. package/src/app/api/openclaw/agent-files/route.ts +3 -1
  41. package/src/app/api/openclaw/approvals/route.ts +3 -1
  42. package/src/app/api/openclaw/config-sync/route.ts +3 -1
  43. package/src/app/api/openclaw/cron/route.ts +4 -2
  44. package/src/app/api/openclaw/exec-config/route.ts +3 -1
  45. package/src/app/api/openclaw/gateway/route.ts +4 -2
  46. package/src/app/api/openclaw/history/route.ts +3 -1
  47. package/src/app/api/openclaw/permissions/route.ts +3 -1
  48. package/src/app/api/openclaw/sandbox-env/route.ts +3 -1
  49. package/src/app/api/openclaw/skills/install/route.ts +3 -1
  50. package/src/app/api/openclaw/skills/remove/route.ts +3 -1
  51. package/src/app/api/openclaw/skills/route.ts +5 -2
  52. package/src/app/api/openclaw/sync/route.ts +5 -3
  53. package/src/app/api/preview-server/route.ts +4 -1
  54. package/src/app/api/projects/[id]/route.ts +3 -1
  55. package/src/app/api/projects/route.ts +3 -1
  56. package/src/app/api/providers/[id]/models/route.ts +3 -1
  57. package/src/app/api/providers/[id]/route.ts +3 -1
  58. package/src/app/api/providers/route.ts +3 -1
  59. package/src/app/api/schedules/[id]/route.ts +3 -1
  60. package/src/app/api/schedules/route.ts +3 -1
  61. package/src/app/api/secrets/[id]/route.ts +3 -1
  62. package/src/app/api/secrets/route.ts +3 -1
  63. package/src/app/api/settings/route.ts +3 -1
  64. package/src/app/api/skills/[id]/route.ts +3 -1
  65. package/src/app/api/skills/route.ts +3 -1
  66. package/src/app/api/souls/[id]/route.ts +3 -1
  67. package/src/app/api/souls/route.ts +3 -1
  68. package/src/app/api/tasks/[id]/route.ts +26 -1
  69. package/src/app/api/tasks/bulk/route.ts +3 -1
  70. package/src/app/api/tasks/claim/route.ts +4 -2
  71. package/src/app/api/tasks/route.ts +20 -3
  72. package/src/app/api/uploads/route.ts +3 -1
  73. package/src/app/api/wallets/[id]/approve/route.ts +3 -1
  74. package/src/app/api/wallets/[id]/route.ts +4 -2
  75. package/src/app/api/wallets/[id]/send/route.ts +3 -1
  76. package/src/app/api/wallets/route.ts +15 -1
  77. package/src/app/globals.css +20 -0
  78. package/src/app/org-chart/page.tsx +11 -0
  79. package/src/cli/index.js +9 -0
  80. package/src/cli/spec.js +7 -0
  81. package/src/components/agents/agent-sheet.tsx +41 -7
  82. package/src/components/chat/markdown-utils.ts +2 -9
  83. package/src/components/chat/message-bubble.tsx +161 -272
  84. package/src/components/chatrooms/chatroom-message.tsx +64 -174
  85. package/src/components/layout/sidebar-rail.tsx +10 -0
  86. package/src/components/org-chart/mini-chat-bubble.tsx +267 -0
  87. package/src/components/org-chart/org-chart-context-menu.tsx +138 -0
  88. package/src/components/org-chart/org-chart-detail-panel.tsx +288 -0
  89. package/src/components/org-chart/org-chart-edge.tsx +66 -0
  90. package/src/components/org-chart/org-chart-node.tsx +281 -0
  91. package/src/components/org-chart/org-chart-sidebar.tsx +552 -0
  92. package/src/components/org-chart/org-chart-speech-bubble.tsx +37 -0
  93. package/src/components/org-chart/org-chart-team-panel.tsx +414 -0
  94. package/src/components/org-chart/org-chart-team-region.tsx +155 -0
  95. package/src/components/org-chart/org-chart-toolbar.tsx +52 -0
  96. package/src/components/org-chart/org-chart-view.tsx +885 -0
  97. package/src/components/org-chart/use-org-chart-drag.ts +87 -0
  98. package/src/components/org-chart/use-org-chart-pan-zoom.ts +108 -0
  99. package/src/components/shared/attachment-chip.tsx +57 -0
  100. package/src/components/shared/markdown-body.tsx +115 -0
  101. package/src/components/shared/markdown-utils.ts +9 -0
  102. package/src/components/shared/message-actions.tsx +102 -0
  103. package/src/lib/agents.ts +3 -0
  104. package/src/lib/app/navigation.test.ts +74 -0
  105. package/src/lib/app/navigation.ts +1 -0
  106. package/src/lib/app/view-constants.ts +9 -1
  107. package/src/lib/fetch-dedup.test.ts +95 -0
  108. package/src/lib/org-chart.ts +299 -0
  109. package/src/lib/personality-parser.test.ts +116 -0
  110. package/src/lib/providers/anthropic.ts +7 -13
  111. package/src/lib/providers/ollama.ts +6 -9
  112. package/src/lib/providers/openai.ts +12 -15
  113. package/src/lib/providers/openclaw.ts +3 -2
  114. package/src/lib/providers/provider-defaults.ts +24 -0
  115. package/src/lib/server/agents/agent-runtime-config.test.ts +7 -1
  116. package/src/lib/server/agents/capability-match.test.ts +75 -0
  117. package/src/lib/server/agents/capability-match.ts +47 -0
  118. package/src/lib/server/agents/main-agent-loop.test.ts +1 -1
  119. package/src/lib/server/agents/main-agent-loop.ts +16 -2
  120. package/src/lib/server/agents/subagent-lineage.test.ts +40 -129
  121. package/src/lib/server/agents/subagent-lineage.ts +17 -69
  122. package/src/lib/server/agents/subagent-runtime.test.ts +21 -19
  123. package/src/lib/server/agents/subagent-runtime.ts +35 -9
  124. package/src/lib/server/build-llm.ts +1 -1
  125. package/src/lib/server/chat-execution/chat-execution.ts +20 -17
  126. package/src/lib/server/chat-execution/chat-streaming-utils.ts +53 -1
  127. package/src/lib/server/chat-execution/chat-turn-state.ts +96 -0
  128. package/src/lib/server/chat-execution/continuation-evaluator.ts +383 -0
  129. package/src/lib/server/chat-execution/continuation-limits.test.ts +123 -0
  130. package/src/lib/server/chat-execution/continuation-limits.ts +116 -0
  131. package/src/lib/server/chat-execution/iteration-event-handler.ts +360 -0
  132. package/src/lib/server/chat-execution/iteration-timers.ts +71 -0
  133. package/src/lib/server/chat-execution/message-classifier.ts +274 -0
  134. package/src/lib/server/chat-execution/post-stream-finalization.ts +183 -0
  135. package/src/lib/server/chat-execution/prompt-budget.ts +60 -0
  136. package/src/lib/server/chat-execution/prompt-builder.ts +456 -0
  137. package/src/lib/server/chat-execution/prompt-mode.ts +26 -0
  138. package/src/lib/server/chat-execution/prompt-sections.test.ts +162 -0
  139. package/src/lib/server/chat-execution/prompt-sections.ts +354 -0
  140. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +190 -11
  141. package/src/lib/server/chat-execution/stream-agent-chat.ts +310 -1610
  142. package/src/lib/server/chat-execution/stream-continuation.test.ts +294 -0
  143. package/src/lib/server/chat-execution/stream-continuation.ts +18 -3
  144. package/src/lib/server/chatrooms/chatroom-helpers.test.ts +80 -0
  145. package/src/lib/server/chatrooms/chatroom-helpers.ts +55 -11
  146. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +134 -0
  147. package/src/lib/server/chatrooms/chatroom-routing.ts +2 -1
  148. package/src/lib/server/connectors/connector-inbound.ts +1498 -0
  149. package/src/lib/server/connectors/connector-lifecycle.ts +482 -0
  150. package/src/lib/server/connectors/connector-outbound.ts +404 -0
  151. package/src/lib/server/connectors/manager.ts +55 -2943
  152. package/src/lib/server/cost.ts +26 -7
  153. package/src/lib/server/missions/mission-service.ts +9 -0
  154. package/src/lib/server/openclaw/sync.ts +4 -3
  155. package/src/lib/server/protocols/protocol-agent-turn.ts +422 -0
  156. package/src/lib/server/protocols/protocol-foreach.test.ts +153 -0
  157. package/src/lib/server/protocols/protocol-foreach.ts +263 -0
  158. package/src/lib/server/protocols/protocol-normalization.test.ts +316 -0
  159. package/src/lib/server/protocols/protocol-normalization.ts +574 -0
  160. package/src/lib/server/protocols/protocol-queries.ts +118 -0
  161. package/src/lib/server/protocols/protocol-run-lifecycle.ts +649 -0
  162. package/src/lib/server/protocols/protocol-service.ts +55 -3937
  163. package/src/lib/server/protocols/protocol-step-helpers.test.ts +144 -0
  164. package/src/lib/server/protocols/protocol-step-helpers.ts +543 -0
  165. package/src/lib/server/protocols/protocol-step-processors.ts +727 -0
  166. package/src/lib/server/protocols/protocol-subflow.ts +244 -0
  167. package/src/lib/server/protocols/protocol-swarm.ts +353 -0
  168. package/src/lib/server/protocols/protocol-templates.ts +259 -0
  169. package/src/lib/server/protocols/protocol-types.ts +149 -0
  170. package/src/lib/server/provider-health.ts +4 -1
  171. package/src/lib/server/runtime/heartbeat-service.ts +1 -1
  172. package/src/lib/server/runtime/queue.ts +70 -23
  173. package/src/lib/server/safe-parse-body.test.ts +53 -0
  174. package/src/lib/server/safe-parse-body.ts +16 -0
  175. package/src/lib/server/session-reset-policy.test.ts +177 -85
  176. package/src/lib/server/session-reset-policy.ts +1 -1
  177. package/src/lib/server/session-tools/chatroom.ts +175 -15
  178. package/src/lib/server/session-tools/context.ts +4 -4
  179. package/src/lib/server/session-tools/index.ts +2 -0
  180. package/src/lib/server/session-tools/protocol.ts +286 -0
  181. package/src/lib/server/storage-auth.ts +71 -0
  182. package/src/lib/server/storage-cache.ts +142 -0
  183. package/src/lib/server/storage-locks.ts +67 -0
  184. package/src/lib/server/storage-normalization.test.ts +219 -0
  185. package/src/lib/server/storage-normalization.ts +493 -0
  186. package/src/lib/server/storage.ts +70 -689
  187. package/src/lib/server/tool-aliases.ts +1 -0
  188. package/src/lib/server/tool-loop-detection.ts +11 -11
  189. package/src/stores/set-if-changed.test.ts +72 -0
  190. package/src/stores/slices/agent-slice.ts +41 -51
  191. package/src/stores/slices/data-slice.ts +38 -140
  192. package/src/stores/slices/session-slice.ts +21 -37
  193. package/src/stores/slices/task-slice.ts +1 -1
  194. package/src/stores/store-utils.test.ts +128 -0
  195. package/src/stores/store-utils.ts +50 -0
  196. package/src/stores/use-chat-store.ts +1 -0
  197. package/src/stores/use-chatroom-store.ts +13 -0
  198. package/src/types/index.ts +29 -1
  199. package/bin/swarmclaw.mjs +0 -1504
package/README.md CHANGED
@@ -177,6 +177,18 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
177
177
 
178
178
  ## Release Notes
179
179
 
180
+ ### v1.1.6 Highlights
181
+
182
+ - **Org chart view**: visual agent hierarchy with drag-and-drop reparenting, team grouping, and context-menu actions for managing agent relationships directly from the canvas.
183
+ - **Dashboard API**: server-side metrics endpoint with cost tracking, usage aggregation, and budget warning thresholds for operator visibility.
184
+ - **Subagent lifecycle overhaul**: state-machine lineage tracking, `delegationDepth` limits, auto-announce on spawn, and cleaner parent-child session management.
185
+ - **Chat execution refactor**: composable prompt sections replace monolithic prompt building, continuation evaluator consolidation, and extracted stream-continuation logic for maintainability.
186
+ - **Per-agent cost attribution**: token costs are tracked and attributed per agent, enabling budget controls and cost reporting at the agent level.
187
+ - **Capability-based task routing**: tasks can match agents by declared capabilities, not just explicit assignment, enabling smarter automatic dispatch.
188
+ - **Bulk agent operations**: new `/api/agents/bulk` endpoint for batch updates across multiple agents in a single request.
189
+ - **Document revisions API**: version history for documents with `/api/documents/[id]/revisions` endpoint.
190
+ - **Store loader consolidation**: async loaders now use `createLoader()` and `setIfChanged` to eliminate redundant re-renders from polling.
191
+
180
192
  ### v1.1.4 Highlights
181
193
 
182
194
  - **Orchestrator agents return as a first-class autonomy mode**: eligible agents can now run scheduled orchestrator wake cycles with their own mission, governance policy, wake interval, cycle cap, Autonomy-desk controls, and setup/editor support.
@@ -189,6 +201,19 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
189
201
  - **Release integrity repair**: `build:ci` no longer trips over the langgraph checkpoint duplicate-column path, which restores clean build validation for the release line.
190
202
  - **Storage writes are safer**: credential and agent saves were tightened to upsert-only behavior and bulk-delete safety guards so tests or scripts cannot accidentally wipe live state.
191
203
  - **Plugin-to-extension cleanup finished**: remaining rename residue in scripts and tests was cleaned up so packaging and release tooling stay aligned with the current extensions model.
204
+ - **Safe body parsing utility**: shared `safeParseBody()` replaces scattered `await req.json()` try/catch blocks across API routes.
205
+
206
+ ### v1.1.4 Highlights
207
+
208
+ - **Orchestrator agents as a real agent mode**: eligible agents can now run scheduled orchestrator wake cycles with their own mission, governance policy, wake interval, and cycle cap.
209
+ - **Runtime durability and recovery**: configurable parallel task execution, stuck-task idle timeout detection, orphaned running-task recovery on startup, and restart-safe swarm/provider-health persistence.
210
+ - **Failover and safety improvements**: provider errors classified for smarter routing, agent budget limits block task execution before it starts.
211
+
212
+ ### v1.1.3 Highlights
213
+
214
+ - **`build:ci` repair**: fixed the langgraph checkpoint duplicate-column crash that blocked CI/build validation.
215
+ - **Safer storage writes**: credentials and agents use upsert-only save behavior, and a collection safety guard blocks accidental bulk-delete paths.
216
+ >>>>>>> Stashed changes
192
217
 
193
218
  ### v1.1.2 Highlights
194
219
 
@@ -98,11 +98,14 @@ function tryRealpath(targetPath) {
98
98
  }
99
99
  }
100
100
 
101
+ const isWindows = process.platform === 'win32'
102
+
101
103
  function runRootCommand(command, args, execImpl = execFileSync) {
102
104
  try {
103
105
  return String(execImpl(command, args, {
104
106
  encoding: 'utf8',
105
107
  stdio: ['ignore', 'pipe', 'pipe'],
108
+ ...(isWindows && { shell: true }),
106
109
  })).trim()
107
110
  } catch {
108
111
  return null
package/bin/server-cmd.js CHANGED
@@ -154,6 +154,8 @@ function resolveInstalledNext(pkgRoot = PKG_ROOT) {
154
154
  }
155
155
  }
156
156
 
157
+ const isWindows = process.platform === 'win32'
158
+
157
159
  function ensurePackageDependencies(pkgRoot = PKG_ROOT) {
158
160
  const resolved = resolveInstalledNext(pkgRoot)
159
161
  if (resolved && fs.existsSync(resolved.nextCli)) return resolved
@@ -161,7 +163,7 @@ function ensurePackageDependencies(pkgRoot = PKG_ROOT) {
161
163
  const packageManager = detectPackageManager(pkgRoot, process.env)
162
164
  const install = getInstallCommand(packageManager)
163
165
  log(`Installing dependencies with ${packageManager}...`)
164
- execFileSync(install.command, install.args, { cwd: pkgRoot, stdio: 'inherit' })
166
+ execFileSync(install.command, install.args, { cwd: pkgRoot, stdio: 'inherit', ...(isWindows && { shell: true }) })
165
167
 
166
168
  const installed = resolveInstalledNext(pkgRoot)
167
169
  if (installed && fs.existsSync(installed.nextCli)) return installed
package/bin/update-cmd.js CHANGED
@@ -16,6 +16,8 @@ const {
16
16
  resolvePackageRoot,
17
17
  } = require('./install-root.js')
18
18
 
19
+ const isWindows = process.platform === 'win32'
20
+
19
21
  const PKG_ROOT = resolvePackageRoot({
20
22
  moduleDir: __dirname,
21
23
  argv1: process.argv[1],
@@ -83,6 +85,7 @@ function runRegistrySelfUpdate(
83
85
  cwd: PKG_ROOT,
84
86
  stdio: 'inherit',
85
87
  timeout: 120_000,
88
+ ...(isWindows && { shell: true }),
86
89
  })
87
90
  logger.log(`Global update complete via ${packageManager}.`)
88
91
  } catch (err) {
@@ -170,7 +173,7 @@ If running from a registry install, update the global package with its owning pa
170
173
  const packageManager = detectPackageManager(PKG_ROOT, process.env)
171
174
  const install = getInstallCommand(packageManager, true)
172
175
  log(`Package files changed — running ${packageManager} install...`)
173
- execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000 })
176
+ execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000, ...(isWindows && { shell: true }) })
174
177
  }
175
178
  } catch {
176
179
  // If diff fails, skip install check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -69,6 +69,7 @@
69
69
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs",
70
70
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
71
71
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
72
+ "test:e2e": "tsx .workbench/browser-e2e/run.ts",
72
73
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
73
74
  "prepack": "npm run build:ci",
74
75
  "postinstall": "node ./scripts/postinstall.mjs"
@@ -4,6 +4,7 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
6
 
7
+ const isWindows = process.platform === 'win32'
7
8
  const args = new Set(process.argv.slice(2))
8
9
  const startAfterSetup = args.has('--start') || args.has('--prod')
9
10
  const productionMode = args.has('--prod')
@@ -25,6 +26,7 @@ function run(command, commandArgs, options = {}) {
25
26
  const result = spawnSync(command, commandArgs, {
26
27
  cwd,
27
28
  stdio: 'inherit',
29
+ ...(isWindows && { shell: true }),
28
30
  ...options,
29
31
  })
30
32
  if (result.error) fail(result.error.message)
@@ -39,6 +41,7 @@ function runOptional(command, commandArgs, options = {}) {
39
41
  const result = spawnSync(command, commandArgs, {
40
42
  cwd,
41
43
  stdio: 'inherit',
44
+ ...(isWindows && { shell: true }),
42
45
  ...options,
43
46
  })
44
47
  if (result.error || (result.status ?? 1) !== 0) {
@@ -60,7 +63,7 @@ function ensureNodeVersion() {
60
63
  }
61
64
 
62
65
  function ensureNpm() {
63
- const result = spawnSync('npm', ['--version'], { cwd, encoding: 'utf8' })
66
+ const result = spawnSync('npm', ['--version'], { cwd, encoding: 'utf8', ...(isWindows && { shell: true }) })
64
67
  if (result.error || (result.status ?? 1) !== 0) {
65
68
  fail('npm was not found. Install npm and rerun this setup command.')
66
69
  }
@@ -69,7 +72,7 @@ function ensureNpm() {
69
72
 
70
73
  function commandExists(name) {
71
74
  const lookup = process.platform === 'win32' ? 'where' : 'which'
72
- const result = spawnSync(lookup, [name], { cwd, encoding: 'utf8' })
75
+ const result = spawnSync(lookup, [name], { cwd, encoding: 'utf8', ...(isWindows && { shell: true }) })
73
76
  return !result.error && (result.status ?? 1) === 0
74
77
  }
75
78
 
@@ -6,6 +6,7 @@ export async function GET(req: Request) {
6
6
  const { searchParams } = new URL(req.url)
7
7
  const entityType = searchParams.get('entityType')
8
8
  const entityId = searchParams.get('entityId')
9
+ const actor = searchParams.get('actor')
9
10
  const action = searchParams.get('action')
10
11
  const since = searchParams.get('since')
11
12
  const limit = Math.min(200, Math.max(1, Number(searchParams.get('limit')) || 50))
@@ -15,6 +16,7 @@ export async function GET(req: Request) {
15
16
 
16
17
  if (entityType) entries = entries.filter((e) => e.entityType === entityType)
17
18
  if (entityId) entries = entries.filter((e) => e.entityId === entityId)
19
+ if (actor) entries = entries.filter((e) => e.actor === actor)
18
20
  if (action) entries = entries.filter((e) => e.action === action)
19
21
  if (since) {
20
22
  const sinceMs = Number(since)
@@ -8,13 +8,15 @@ import { notify } from '@/lib/server/ws-hub'
8
8
  import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
9
9
  import { normalizeCapabilitySelection } from '@/lib/capability-selection'
10
10
  import { normalizeOrchestratorConfig } from '@/lib/orchestrator-config'
11
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
11
12
 
12
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
14
  const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents', table: 'agents' }
14
15
 
15
16
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
17
  const { id } = await params
17
- const body = await req.json()
18
+ const { data: body, error } = await safeParseBody(req)
19
+ if (error) return error
18
20
  const result = mutateItem(ops, id, (agent) => {
19
21
  Object.assign(agent, body, { updatedAt: Date.now() })
20
22
  if (body.tools !== undefined || body.extensions !== undefined) {
@@ -42,7 +44,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
42
44
  if (body.apiEndpoint !== undefined) {
43
45
  agent.apiEndpoint = normalizeProviderEndpoint(
44
46
  body.provider || agent.provider,
45
- body.apiEndpoint,
47
+ body.apiEndpoint as string | null | undefined,
46
48
  )
47
49
  }
48
50
  if (body.provider !== undefined && body.provider !== 'ollama' && body.ollamaMode === undefined) {
@@ -111,7 +113,6 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
111
113
  }
112
114
  delete (agent as Record<string, unknown>).platformAssignScope
113
115
  delete (agent as Record<string, unknown>).subAgentIds
114
- delete (agent as Record<string, unknown>).isOrchestrator
115
116
  delete (agent as Record<string, unknown>).id
116
117
  agent.id = id
117
118
  return agent
@@ -0,0 +1,55 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
3
+ import { patchAgent } from '@/lib/server/storage'
4
+ import { logActivity } from '@/lib/server/storage'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+
7
+ export async function PATCH(req: Request) {
8
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
9
+ if (error) return error
10
+
11
+ const patches = body.patches
12
+ if (!Array.isArray(patches) || patches.length === 0) {
13
+ return NextResponse.json({ error: 'patches must be a non-empty array' }, { status: 400 })
14
+ }
15
+
16
+ let updated = 0
17
+ const errors: string[] = []
18
+
19
+ for (const entry of patches) {
20
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
21
+ errors.push('Invalid patch entry (not an object)')
22
+ continue
23
+ }
24
+ const { id, patch } = entry as { id?: unknown; patch?: unknown }
25
+ if (typeof id !== 'string' || !id.trim()) {
26
+ errors.push('Patch entry missing valid id')
27
+ continue
28
+ }
29
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
30
+ errors.push(`Patch for ${id} is not a valid object`)
31
+ continue
32
+ }
33
+
34
+ const result = patchAgent(id, (current) => {
35
+ if (!current) return null
36
+ return { ...current, ...(patch as Record<string, unknown>), updatedAt: Date.now() }
37
+ })
38
+
39
+ if (result) {
40
+ updated++
41
+ logActivity({
42
+ entityType: 'agent',
43
+ entityId: id,
44
+ action: 'updated',
45
+ actor: 'user',
46
+ summary: `Bulk patch: updated agent "${result.name || id}"`,
47
+ })
48
+ } else {
49
+ errors.push(`Agent ${id} not found`)
50
+ }
51
+ }
52
+
53
+ if (updated > 0) notify('agents')
54
+ return NextResponse.json({ updated, errors })
55
+ }
@@ -10,6 +10,7 @@ import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
10
10
  import { normalizeOrchestratorConfig } from '@/lib/orchestrator-config'
11
11
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
12
12
  import { z } from 'zod'
13
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
13
14
  export const dynamic = 'force-dynamic'
14
15
 
15
16
  async function ensureDaemonIfNeeded(source: string) {
@@ -57,7 +58,8 @@ export async function GET(req: Request) {
57
58
 
58
59
  export async function POST(req: Request) {
59
60
  await ensureDaemonIfNeeded('api/agents:post')
60
- const raw = await req.json()
61
+ const { data: raw, error } = await safeParseBody(req)
62
+ if (error) return error
61
63
  const rawRecord = raw && typeof raw === 'object' ? raw as Record<string, unknown> : null
62
64
  const parsed = AgentCreateSchema.safeParse(raw)
63
65
  if (!parsed.success) {
@@ -3,6 +3,7 @@ import { loadTrashedAgents, loadAgents, saveAgents, deleteAgent } from '@/lib/se
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { badRequest, notFound } from '@/lib/server/collection-helpers'
5
5
  import { purgeAgentReferences, restoreAgentSchedules } from '@/lib/server/agents/agent-cascade'
6
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
7
 
7
8
  /** GET — list trashed agents */
8
9
  export async function GET() {
@@ -11,7 +12,8 @@ export async function GET() {
11
12
 
12
13
  /** POST { id } — restore a trashed agent */
13
14
  export async function POST(req: Request) {
14
- const body = await req.json()
15
+ const { data: body, error } = await safeParseBody(req)
16
+ if (error) return error
15
17
  const id = body?.id as string | undefined
16
18
  if (!id) return badRequest('Missing agent id')
17
19
 
@@ -35,7 +37,8 @@ export async function POST(req: Request) {
35
37
 
36
38
  /** DELETE { id } — permanently delete a trashed agent */
37
39
  export async function DELETE(req: Request) {
38
- const body = await req.json()
40
+ const { data: body, error: parseError } = await safeParseBody(req)
41
+ if (parseError) return parseError
39
42
  const id = body?.id as string | undefined
40
43
  if (!id) return badRequest('Missing agent id')
41
44
 
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
2
3
  import { getAccessKey, validateAccessKey, isFirstTimeSetup, markSetupComplete, replaceAccessKey } from '@/lib/server/storage'
3
4
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
4
5
  import { isProductionRuntime } from '@/lib/runtime/runtime-env'
@@ -65,9 +66,19 @@ export async function GET(req: Request) {
65
66
  })
66
67
  }
67
68
 
69
+ function pruneExpiredEntries() {
70
+ const now = Date.now()
71
+ for (const [ip, entry] of authRateLimitMap) {
72
+ if (entry.lockedUntil > 0 && entry.lockedUntil < now) {
73
+ authRateLimitMap.delete(ip)
74
+ }
75
+ }
76
+ }
77
+
68
78
  /** POST /api/auth — validate an access key */
69
79
  export async function POST(req: Request) {
70
80
  const rateLimitEnabled = isRateLimitEnabled()
81
+ if (rateLimitEnabled) pruneExpiredEntries()
71
82
  const clientIp = getClientIp(req)
72
83
  const entry = rateLimitEnabled ? authRateLimitMap.get(clientIp) : undefined
73
84
  if (rateLimitEnabled && entry && entry.lockedUntil > Date.now()) {
@@ -78,7 +89,9 @@ export async function POST(req: Request) {
78
89
  ))
79
90
  }
80
91
 
81
- const { key, override } = await req.json()
92
+ const { data: body, error } = await safeParseBody<{ key: string; override?: boolean }>(req)
93
+ if (error) return error
94
+ const { key, override } = body
82
95
 
83
96
  // During first-time setup, allow the user to replace the generated key with their own
84
97
  if (override && isFirstTimeSetup() && typeof key === 'string' && key.trim().length >= 8) {
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { normalizeCanvasContent } from '@/lib/canvas-content'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
6
 
6
7
  export async function GET(_req: Request, { params }: { params: Promise<{ sessionId: string }> }) {
7
8
  const { sessionId } = await params
@@ -17,7 +18,8 @@ export async function GET(_req: Request, { params }: { params: Promise<{ session
17
18
 
18
19
  export async function POST(req: Request, { params }: { params: Promise<{ sessionId: string }> }) {
19
20
  const { sessionId } = await params
20
- const body = await req.json()
21
+ const { data: body, error } = await safeParseBody(req)
22
+ if (error) return error
21
23
  const sessions = loadSessions()
22
24
  const session = sessions[sessionId]
23
25
  if (!session) return NextResponse.json({ error: 'Session not found' }, { status: 404 })
@@ -3,6 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
5
  import { notFound } from '@/lib/server/collection-helpers'
6
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
7
  import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
7
8
  import { getProvider } from '@/lib/providers'
8
9
  import {
@@ -26,6 +27,7 @@ import { resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-conf
26
27
  import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
27
28
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
28
29
  import { errorMessage } from '@/lib/shared-utils'
30
+ import { persistChatroomInteractionMemory } from '@/lib/server/chatrooms/chatroom-memory-bridge'
29
31
 
30
32
  export const dynamic = 'force-dynamic'
31
33
  export const maxDuration = 300
@@ -34,7 +36,8 @@ const MAX_CHAIN_DEPTH = 5
34
36
 
35
37
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
36
38
  const { id } = await params
37
- const body = await req.json()
39
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
40
+ if (error) return error
38
41
 
39
42
  const chatrooms = loadChatrooms()
40
43
  const chatroom = chatrooms[id] as Chatroom | undefined
@@ -57,7 +60,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
57
60
  // Persist incoming message
58
61
  const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
59
62
  const replyTargetAgentId = resolveReplyTargetAgentId(replyToId, chatroom.messages, chatroom.agentIds)
60
- let mentions = parseMentions(text, agents, chatroom.agentIds, { replyTargetAgentId })
63
+ let mentions = parseMentions(text, agents, chatroom.agentIds, { replyTargetAgentId, senderId: senderId !== 'user' ? senderId : null })
61
64
  // Routing rules: if no explicit mentions, evaluate keyword/capability rules
62
65
  if (mentions.length === 0 && chatroom.routingRules?.length) {
63
66
  const agentList = chatroom.agentIds.map((aid) => agents[aid]).filter(Boolean)
@@ -67,6 +70,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
67
70
  if (chatroom.autoAddress && mentions.length === 0) {
68
71
  mentions = [...chatroom.agentIds]
69
72
  }
73
+ // If a specific agent is targeted, ensure they're in the mentions
74
+ const incomingTargetAgentId = typeof body.targetAgentId === 'string' ? body.targetAgentId : undefined
75
+ if (incomingTargetAgentId && chatroom.agentIds.includes(incomingTargetAgentId) && !mentions.includes(incomingTargetAgentId)) {
76
+ mentions.push(incomingTargetAgentId)
77
+ }
70
78
  const mentionHealth = filterHealthyChatroomAgents(mentions, agents)
71
79
  mentions = mentionHealth.healthyAgentIds
72
80
  const userMessage: ChatroomMessage = {
@@ -81,6 +89,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
81
89
  ...(imagePath ? { imagePath } : {}),
82
90
  ...(attachedFiles ? { attachedFiles } : {}),
83
91
  ...(replyToId ? { replyToId } : {}),
92
+ ...(incomingTargetAgentId ? { targetAgentId: incomingTargetAgentId } : {}),
84
93
  }
85
94
  chatroom.messages.push(userMessage)
86
95
  compactChatroomMessages(chatroom)
@@ -90,6 +99,20 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
90
99
  notify('chatrooms')
91
100
  notify(`chatroom:${id}`)
92
101
 
102
+ // If sender is an agent (via triggerResponses tool), just persist the message — don't re-process agents
103
+ if (senderId !== 'user' && agents[senderId]) {
104
+ const encoder = new TextEncoder()
105
+ const noopStream = new ReadableStream({
106
+ start(controller) {
107
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ t: 'done' })}\n\n`))
108
+ controller.close()
109
+ },
110
+ })
111
+ return new NextResponse(noopStream, {
112
+ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' },
113
+ })
114
+ }
115
+
93
116
  // Build reply context if replying to a message
94
117
  let replyContext = ''
95
118
  if (replyToId) {
@@ -243,7 +266,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
243
266
 
244
267
  if (responseText.trim() && !shouldSuppressHiddenControlText(rawResponseText)) {
245
268
  appendSyntheticSessionMessage(syntheticSession.id, 'assistant', responseText)
246
- const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
269
+ const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds, { senderId: agent.id, skipImplicit: true })
247
270
  const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
248
271
  const newMentions = chainedHealth.healthyAgentIds
249
272
  if (chainedHealth.skipped.length > 0) {
@@ -273,6 +296,17 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
273
296
  // Extract and apply reactions (e.g. [REACTION]{"emoji":"👍","to":"..."})
274
297
  applyAgentReactionsFromText(responseText, id, agent.id)
275
298
 
299
+ // Persist interaction to agent memory (fire-and-forget)
300
+ persistChatroomInteractionMemory({
301
+ agentId: agent.id,
302
+ agent,
303
+ chatroomId: id,
304
+ chatroomName: chatroom.name,
305
+ senderName,
306
+ inboundText: text,
307
+ responseText,
308
+ }).catch(() => {})
309
+
276
310
  markProviderSuccess(agent.provider)
277
311
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
278
312
 
@@ -319,7 +353,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
319
353
  )
320
354
  if (lastAgentMsg) {
321
355
  const truncated = lastAgentMsg.text.length > 500 ? lastAgentMsg.text.slice(0, 500) + '...' : lastAgentMsg.text
322
- item.contextMessage = `${lastAgentMsg.senderName} said: "${truncated}" They're requesting your help. Review the conversation and respond.`
356
+ const originalTruncated = text.length > 300 ? text.slice(0, 300) + '...' : text
357
+ item.contextMessage = [
358
+ `[Conversation context] The user said: "${originalTruncated}"`,
359
+ `${lastAgentMsg.senderName} then said: "${truncated}"`,
360
+ `They mentioned you — respond to the conversation naturally.`,
361
+ ].join('\n')
323
362
  }
324
363
  }
325
364
 
@@ -2,16 +2,18 @@ import { NextResponse } from 'next/server'
2
2
  import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
6
  import { genId } from '@/lib/id'
6
7
 
7
8
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
9
  const { id } = await params
9
- const body = await req.json()
10
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
11
+ if (error) return error
10
12
  const chatrooms = loadChatrooms()
11
13
  const chatroom = chatrooms[id]
12
14
  if (!chatroom) return notFound()
13
15
 
14
- const agentId = body.agentId as string
16
+ const agentId = typeof body.agentId === 'string' ? body.agentId : ''
15
17
  if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
16
18
 
17
19
  if (!chatroom.agentIds.includes(agentId)) {
@@ -44,12 +46,13 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
44
46
 
45
47
  export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
46
48
  const { id } = await params
47
- const body = await req.json()
49
+ const { data: body, error: delError } = await safeParseBody<Record<string, unknown>>(req)
50
+ if (delError) return delError
48
51
  const chatrooms = loadChatrooms()
49
52
  const chatroom = chatrooms[id]
50
53
  if (!chatroom) return notFound()
51
54
 
52
- const agentId = body.agentId as string
55
+ const agentId = typeof body.agentId === 'string' ? body.agentId : ''
53
56
  if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
54
57
 
55
58
  const wasPresent = chatroom.agentIds.includes(agentId)
@@ -3,6 +3,7 @@ import crypto from 'crypto'
3
3
  import { loadChatrooms, saveChatrooms, appendModerationLog } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
5
  import { notFound } from '@/lib/server/collection-helpers'
6
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
7
  import { getMembers } from '@/lib/server/chatrooms/chatroom-helpers'
7
8
  import type { Chatroom, ChatroomMember } from '@/types'
8
9
 
@@ -26,7 +27,8 @@ function isValidRole(role: unknown): role is ChatroomMember['role'] {
26
27
 
27
28
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
28
29
  const { id } = await params
29
- const body = await req.json() as Record<string, unknown>
30
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
31
+ if (error) return error
30
32
 
31
33
  const chatrooms = loadChatrooms()
32
34
  const chatroom = chatrooms[id] as Chatroom | undefined
@@ -2,16 +2,18 @@ import { NextResponse } from 'next/server'
2
2
  import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
6
  import type { Chatroom } from '@/types'
6
7
 
7
8
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
9
  const { id } = await params
9
- const body = await req.json()
10
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
11
+ if (error) return error
10
12
  const chatrooms = loadChatrooms()
11
13
  const chatroom = chatrooms[id] as Chatroom | undefined
12
14
  if (!chatroom) return notFound()
13
15
 
14
- const messageId = body.messageId as string
16
+ const messageId = typeof body.messageId === 'string' ? body.messageId : ''
15
17
  if (!messageId) {
16
18
  return NextResponse.json({ error: 'messageId is required' }, { status: 400 })
17
19
  }
@@ -2,18 +2,20 @@ import { NextResponse } from 'next/server'
2
2
  import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
6
  import type { Chatroom, ChatroomMessage, ChatroomReaction } from '@/types'
6
7
 
7
8
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
9
  const { id } = await params
9
- const body = await req.json()
10
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
11
+ if (error) return error
10
12
  const chatrooms = loadChatrooms()
11
13
  const chatroom = chatrooms[id] as Chatroom | undefined
12
14
  if (!chatroom) return notFound()
13
15
 
14
- const messageId = body.messageId as string
15
- const emoji = body.emoji as string
16
- const reactorId = (body.reactorId as string) || 'user'
16
+ const messageId = typeof body.messageId === 'string' ? body.messageId : ''
17
+ const emoji = typeof body.emoji === 'string' ? body.emoji : ''
18
+ const reactorId = typeof body.reactorId === 'string' && body.reactorId ? body.reactorId : 'user'
17
19
  if (!messageId || !emoji) {
18
20
  return NextResponse.json({ error: 'messageId and emoji are required' }, { status: 400 })
19
21
  }
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { loadChatrooms, saveChatrooms, loadAgents, loadConnectors, saveConnectors } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
6
  import { genId } from '@/lib/id'
6
7
 
7
8
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -14,7 +15,8 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
14
15
 
15
16
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
17
  const { id } = await params
17
- const body = await req.json()
18
+ const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
19
+ if (error) return error
18
20
  const chatrooms = loadChatrooms()
19
21
  const chatroom = chatrooms[id]
20
22
  if (!chatroom) return notFound()
@@ -40,7 +42,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
40
42
  )
41
43
  }
42
44
  const agents = loadAgents()
43
- const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
45
+ const agentIds = (body.agentIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
46
+ const invalidAgentIds = agentIds.filter((agentId) => !agents[agentId])
44
47
  if (invalidAgentIds.length > 0) {
45
48
  return NextResponse.json(
46
49
  { error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
@@ -49,8 +52,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
49
52
  }
50
53
 
51
54
  const oldIds = new Set(chatroom.agentIds)
52
- const newIds = new Set(body.agentIds as string[])
53
- const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
55
+ const newIds = new Set(agentIds)
56
+ const added = agentIds.filter((aid: string) => !oldIds.has(aid))
54
57
  const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
55
58
 
56
59
  if (added.length > 0 || removed.length > 0) {
@@ -83,7 +86,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
83
86
  }
84
87
  }
85
88
 
86
- chatroom.agentIds = body.agentIds
89
+ chatroom.agentIds = agentIds
87
90
  }
88
91
 
89
92
  chatroom.updatedAt = Date.now()