@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,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSchedules, saveSchedules, deleteSchedule } from '@/lib/server/storage'
3
+ import { resolveScheduleName } from '@/lib/schedule-name'
4
+
5
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const body = await req.json()
8
+ const schedules = loadSchedules()
9
+ if (!schedules[id]) return new NextResponse(null, { status: 404 })
10
+
11
+ const origId = id
12
+ Object.assign(schedules[id], body)
13
+ schedules[id].id = origId
14
+ schedules[id].name = resolveScheduleName({
15
+ name: schedules[id].name,
16
+ taskPrompt: schedules[id].taskPrompt,
17
+ })
18
+ saveSchedules(schedules)
19
+ return NextResponse.json(schedules[id])
20
+ }
21
+
22
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
23
+ const { id } = await params
24
+ const schedules = loadSchedules()
25
+ if (!schedules[id]) return new NextResponse(null, { status: 404 })
26
+ deleteSchedule(id)
27
+ return NextResponse.json('ok')
28
+ }
@@ -0,0 +1,104 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
4
+ import { enqueueTask } from '@/lib/server/queue'
5
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
6
+ import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
7
+
8
+ type InFlightTask = {
9
+ status?: string
10
+ sourceScheduleKey?: string | null
11
+ }
12
+
13
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
14
+ const { id } = await params
15
+ const schedules = loadSchedules()
16
+ const schedule = schedules[id]
17
+ if (!schedule) return new NextResponse(null, { status: 404 })
18
+
19
+ const agents = loadAgents()
20
+ const agent = agents[schedule.agentId]
21
+ if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
22
+
23
+ const tasks = loadTasks()
24
+ const scheduleSignature = getScheduleSignatureKey(schedule)
25
+ if (scheduleSignature) {
26
+ const inFlight = Object.values(tasks as Record<string, InFlightTask>).some((task) =>
27
+ task
28
+ && (task.status === 'queued' || task.status === 'running')
29
+ && task.sourceScheduleKey === scheduleSignature,
30
+ )
31
+ if (inFlight) {
32
+ return NextResponse.json({ ok: true, queued: false, reason: 'in_flight' })
33
+ }
34
+ }
35
+
36
+ const now = Date.now()
37
+ schedule.runNumber = (schedule.runNumber || 0) + 1
38
+
39
+ // Reuse linked task if it exists and is not in-flight
40
+ let taskId = ''
41
+ const existingTaskId = typeof schedule.linkedTaskId === 'string' ? schedule.linkedTaskId : ''
42
+ const existingTask = existingTaskId ? tasks[existingTaskId] : null
43
+
44
+ if (existingTask && existingTask.status !== 'queued' && existingTask.status !== 'running') {
45
+ taskId = existingTaskId
46
+ const prev = existingTask as Record<string, unknown>
47
+ prev.totalRuns = ((prev.totalRuns as number) || 0) + 1
48
+ if (existingTask.status === 'completed') prev.totalCompleted = ((prev.totalCompleted as number) || 0) + 1
49
+ if (existingTask.status === 'failed') prev.totalFailed = ((prev.totalFailed as number) || 0) + 1
50
+
51
+ existingTask.status = 'backlog'
52
+ existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
53
+ existingTask.result = null
54
+ existingTask.error = null
55
+ existingTask.sessionId = null
56
+ existingTask.updatedAt = now
57
+ existingTask.queuedAt = null
58
+ existingTask.startedAt = null
59
+ existingTask.completedAt = null
60
+ existingTask.archivedAt = null
61
+ existingTask.attempts = 0
62
+ existingTask.retryScheduledAt = null
63
+ existingTask.deadLetteredAt = null
64
+ existingTask.validation = null
65
+ prev.runNumber = schedule.runNumber
66
+ } else {
67
+ taskId = crypto.randomBytes(4).toString('hex')
68
+ tasks[taskId] = {
69
+ id: taskId,
70
+ title: `[Sched] ${schedule.name} (run #${schedule.runNumber})`,
71
+ description: schedule.taskPrompt || '',
72
+ status: 'backlog',
73
+ agentId: schedule.agentId,
74
+ sessionId: null,
75
+ result: null,
76
+ error: null,
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ queuedAt: null,
80
+ startedAt: null,
81
+ completedAt: null,
82
+ sourceType: 'schedule',
83
+ sourceScheduleId: schedule.id,
84
+ sourceScheduleName: schedule.name,
85
+ sourceScheduleKey: scheduleSignature || null,
86
+ createdInSessionId: schedule.createdInSessionId || null,
87
+ createdByAgentId: schedule.createdByAgentId || null,
88
+ runNumber: schedule.runNumber,
89
+ }
90
+ schedule.linkedTaskId = taskId
91
+ }
92
+
93
+ saveTasks(tasks)
94
+ enqueueTask(taskId)
95
+ pushMainLoopEventToMainSessions({
96
+ type: 'schedule_fired',
97
+ text: `Schedule fired manually: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
98
+ })
99
+
100
+ schedule.lastRunAt = now
101
+ saveSchedules(schedules)
102
+
103
+ return NextResponse.json({ ok: true, queued: true, taskId, runNumber: schedule.runNumber })
104
+ }
@@ -0,0 +1,78 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadSchedules, saveSchedules } from '@/lib/server/storage'
4
+ import { resolveScheduleName } from '@/lib/schedule-name'
5
+ import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
6
+
7
+ export async function GET() {
8
+ return NextResponse.json(loadSchedules())
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ const body = await req.json()
13
+ const now = Date.now()
14
+ const schedules = loadSchedules()
15
+ const scheduleType = body.scheduleType || 'cron'
16
+
17
+ const duplicate = findDuplicateSchedule(schedules, {
18
+ agentId: body.agentId || null,
19
+ taskPrompt: body.taskPrompt || '',
20
+ scheduleType,
21
+ cron: body.cron,
22
+ intervalMs: body.intervalMs,
23
+ runAt: body.runAt,
24
+ })
25
+ if (duplicate) {
26
+ const duplicateId = duplicate.id || ''
27
+ let changed = false
28
+ const nextName = resolveScheduleName({
29
+ name: body.name ?? duplicate.name,
30
+ taskPrompt: body.taskPrompt ?? duplicate.taskPrompt,
31
+ })
32
+ if (nextName && nextName !== duplicate.name) {
33
+ duplicate.name = nextName
34
+ changed = true
35
+ }
36
+ const normalizedStatus = typeof body.status === 'string' ? body.status.trim().toLowerCase() : ''
37
+ if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
38
+ duplicate.status = normalizedStatus as 'active' | 'paused'
39
+ changed = true
40
+ }
41
+ if (changed) {
42
+ const mutableDuplicate = duplicate as Record<string, unknown>
43
+ mutableDuplicate.updatedAt = now
44
+ if (duplicateId) schedules[duplicateId] = duplicate
45
+ saveSchedules(schedules)
46
+ }
47
+ return NextResponse.json(duplicate)
48
+ }
49
+
50
+ const id = crypto.randomBytes(4).toString('hex')
51
+
52
+ let nextRunAt: number | undefined
53
+ if (scheduleType === 'once' && body.runAt) {
54
+ nextRunAt = body.runAt
55
+ } else if (scheduleType === 'interval' && body.intervalMs) {
56
+ nextRunAt = now + body.intervalMs
57
+ } else if (scheduleType === 'cron') {
58
+ // nextRunAt will be computed by the scheduler engine
59
+ nextRunAt = undefined
60
+ }
61
+
62
+ schedules[id] = {
63
+ id,
64
+ name: resolveScheduleName({ name: body.name, taskPrompt: body.taskPrompt }),
65
+ agentId: body.agentId,
66
+ taskPrompt: body.taskPrompt || '',
67
+ scheduleType,
68
+ cron: body.cron,
69
+ intervalMs: body.intervalMs,
70
+ runAt: body.runAt,
71
+ lastRunAt: undefined,
72
+ nextRunAt,
73
+ status: body.status || 'active',
74
+ createdAt: now,
75
+ }
76
+ saveSchedules(schedules)
77
+ return NextResponse.json(schedules[id])
78
+ }
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSecrets, saveSecrets } from '@/lib/server/storage'
3
+
4
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params
6
+ const secrets = loadSecrets()
7
+ if (!secrets[id]) return new NextResponse(null, { status: 404 })
8
+ delete secrets[id]
9
+ saveSecrets(secrets)
10
+ return NextResponse.json('ok')
11
+ }
12
+
13
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
+ const { id } = await params
15
+ const body = await req.json()
16
+ const secrets = loadSecrets()
17
+ if (!secrets[id]) return new NextResponse(null, { status: 404 })
18
+
19
+ // Update metadata only (not the encrypted value unless a new value is provided)
20
+ if (body.name !== undefined) secrets[id].name = body.name
21
+ if (body.service !== undefined) secrets[id].service = body.service
22
+ if (body.scope !== undefined) secrets[id].scope = body.scope
23
+ if (body.agentIds !== undefined) secrets[id].agentIds = body.agentIds
24
+ secrets[id].updatedAt = Date.now()
25
+ saveSecrets(secrets)
26
+
27
+ const { encryptedValue, ...safe } = secrets[id]
28
+ return NextResponse.json(safe)
29
+ }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadSecrets, saveSecrets, encryptKey } from '@/lib/server/storage'
4
+
5
+ export async function GET() {
6
+ // Return secrets WITHOUT the encrypted values (just metadata)
7
+ const secrets = loadSecrets()
8
+ const safe = Object.fromEntries(
9
+ Object.entries(secrets).map(([id, s]: [string, any]) => [
10
+ id,
11
+ { id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, createdAt: s.createdAt, updatedAt: s.updatedAt },
12
+ ])
13
+ )
14
+ return NextResponse.json(safe)
15
+ }
16
+
17
+ export async function POST(req: Request) {
18
+ const body = await req.json()
19
+ const id = crypto.randomBytes(4).toString('hex')
20
+ const now = Date.now()
21
+ const secrets = loadSecrets()
22
+
23
+ if (!body.value?.trim()) {
24
+ return NextResponse.json({ error: 'value is required' }, { status: 400 })
25
+ }
26
+
27
+ secrets[id] = {
28
+ id,
29
+ name: body.name || 'Unnamed Secret',
30
+ service: body.service || 'custom',
31
+ encryptedValue: encryptKey(body.value),
32
+ scope: body.scope || 'global',
33
+ agentIds: body.agentIds || [],
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ }
37
+ saveSecrets(secrets)
38
+
39
+ // Return without encrypted value
40
+ const { encryptedValue, ...safe } = secrets[id]
41
+ return NextResponse.json(safe)
42
+ }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { hasActiveBrowser, cleanupSessionBrowser } from '@/lib/server/session-tools'
3
+
4
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params
6
+ return NextResponse.json({ active: hasActiveBrowser(id) })
7
+ }
8
+
9
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ cleanupSessionBrowser(id)
12
+ return new NextResponse('OK')
13
+ }
@@ -0,0 +1,96 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { enqueueSessionRun, type SessionQueueMode } from '@/lib/server/session-run-manager'
3
+ import { log } from '@/lib/server/logger'
4
+
5
+ function normalizeQueueMode(raw: unknown, internal: boolean): SessionQueueMode {
6
+ if (raw === 'steer' || raw === 'collect' || raw === 'followup') return raw
7
+ return internal ? 'collect' : 'followup'
8
+ }
9
+
10
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const body = await req.json().catch(() => ({}))
13
+
14
+ const message = typeof body.message === 'string' ? body.message : ''
15
+ const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
16
+ const imageUrl = typeof body.imageUrl === 'string' ? body.imageUrl : undefined
17
+ const internal = body.internal === true
18
+ const queueMode = normalizeQueueMode(body.queueMode, internal)
19
+
20
+ if (!message.trim()) {
21
+ return NextResponse.json({ error: 'message is required' }, { status: 400 })
22
+ }
23
+
24
+ const encoder = new TextEncoder()
25
+ const stream = new ReadableStream({
26
+ start(controller) {
27
+ let closed = false
28
+ const writeEvent = (event: Record<string, unknown>) => {
29
+ if (closed) return
30
+ try {
31
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
32
+ } catch {
33
+ closed = true
34
+ }
35
+ }
36
+
37
+ const run = enqueueSessionRun({
38
+ sessionId: id,
39
+ message,
40
+ imagePath,
41
+ imageUrl,
42
+ internal,
43
+ source: internal ? 'heartbeat' : 'chat',
44
+ mode: queueMode,
45
+ onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
46
+ })
47
+
48
+ log.info('chat', `Enqueued session run ${run.runId}`, {
49
+ sessionId: id,
50
+ internal,
51
+ mode: queueMode,
52
+ position: run.position,
53
+ deduped: run.deduped || false,
54
+ coalesced: run.coalesced || false,
55
+ })
56
+
57
+ writeEvent({
58
+ t: 'md',
59
+ text: JSON.stringify({
60
+ run: {
61
+ id: run.runId,
62
+ status: run.deduped ? 'deduped' : run.coalesced ? 'coalesced' : 'queued',
63
+ position: run.position,
64
+ internal,
65
+ mode: queueMode,
66
+ },
67
+ }),
68
+ })
69
+
70
+ run.promise
71
+ .catch((err) => {
72
+ const msg = err?.message || String(err)
73
+ writeEvent({ t: 'err', text: msg })
74
+ })
75
+ .finally(() => {
76
+ writeEvent({ t: 'done' })
77
+ if (!closed) {
78
+ try { controller.close() } catch { /* stream already closed */ }
79
+ closed = true
80
+ }
81
+ })
82
+ },
83
+ cancel() {
84
+ // Client disconnected; subsequent writes should be ignored.
85
+ },
86
+ })
87
+
88
+ return new NextResponse(stream, {
89
+ headers: {
90
+ 'Content-Type': 'text/event-stream',
91
+ 'Cache-Control': 'no-cache',
92
+ 'Connection': 'keep-alive',
93
+ 'X-Accel-Buffering': 'no',
94
+ },
95
+ })
96
+ }
@@ -0,0 +1,19 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+
4
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params
6
+ const sessions = loadSessions()
7
+ if (!sessions[id]) return new NextResponse(null, { status: 404 })
8
+ sessions[id].messages = []
9
+ sessions[id].claudeSessionId = null
10
+ sessions[id].codexThreadId = null
11
+ sessions[id].opencodeSessionId = null
12
+ sessions[id].delegateResumeIds = {
13
+ claudeCode: null,
14
+ codex: null,
15
+ opencode: null,
16
+ }
17
+ saveSessions(sessions)
18
+ return new NextResponse('OK')
19
+ }
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { execSync } from 'child_process'
3
+ import { loadSessions } from '@/lib/server/storage'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const sessions = loadSessions()
8
+ const session = sessions[id]
9
+ if (!session) return new NextResponse(null, { status: 404 })
10
+
11
+ const body = await req.json()
12
+ const msg = body.message || 'Deploy from SwarmClaw'
13
+
14
+ try {
15
+ const opts = { cwd: session.cwd, encoding: 'utf8' as const, timeout: 30000 }
16
+ execSync('git add -A', opts)
17
+ let committed = false
18
+ try {
19
+ execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, opts)
20
+ committed = true
21
+ } catch (ce: any) {
22
+ if (!(ce.stdout || ce.stderr || '').includes('nothing to commit')) throw ce
23
+ }
24
+ execSync('git push 2>&1', opts)
25
+ console.log(`[${id}] deployed: ${msg}`)
26
+ return NextResponse.json({ ok: true, output: committed ? 'Committed and pushed!' : 'Already committed — pushed to remote!' })
27
+ } catch (e: any) {
28
+ console.error(`[${id}] deploy error:`, e.message)
29
+ return NextResponse.json(
30
+ { ok: false, error: (e.stderr || e.stdout || e.message).toString().slice(0, 300) },
31
+ { status: 500 },
32
+ )
33
+ }
34
+ }
@@ -0,0 +1,69 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { spawn } from 'child_process'
3
+ import { loadSessions, devServers, localIP } from '@/lib/server/storage'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const sessions = loadSessions()
8
+ const session = sessions[id]
9
+ if (!session) return new NextResponse(null, { status: 404 })
10
+
11
+ const { action } = await req.json()
12
+
13
+ if (action === 'start') {
14
+ if (devServers.has(id)) {
15
+ const ds = devServers.get(id)!
16
+ return NextResponse.json({ running: true, url: ds.url })
17
+ }
18
+
19
+ const proc = spawn('npm', ['run', 'dev', '--', '--host', '0.0.0.0'], {
20
+ cwd: session.cwd,
21
+ stdio: ['ignore', 'pipe', 'pipe'],
22
+ env: { ...process.env, FORCE_COLOR: '0' },
23
+ })
24
+
25
+ let output = ''
26
+ let detectedUrl: string | null = null
27
+ const urlRe = /https?:\/\/(?:localhost|0\.0\.0\.0|[\d.]+):(\d+)/
28
+
29
+ function onData(chunk: Buffer) {
30
+ output += chunk.toString()
31
+ if (!detectedUrl) {
32
+ const match = output.match(urlRe)
33
+ if (match) {
34
+ const port = match[1]
35
+ detectedUrl = `http://${localIP()}:${port}`
36
+ const ds = devServers.get(id)
37
+ if (ds) ds.url = detectedUrl
38
+ }
39
+ }
40
+ }
41
+
42
+ proc.stdout!.on('data', onData)
43
+ proc.stderr!.on('data', onData)
44
+ proc.on('close', () => { devServers.delete(id); console.log(`[${id}] dev server stopped`) })
45
+ proc.on('error', () => devServers.delete(id))
46
+
47
+ devServers.set(id, { proc, url: `http://${localIP()}:4321` })
48
+ console.log(`[${id}] starting dev server in ${session.cwd}`)
49
+
50
+ // Wait for URL detection
51
+ await new Promise(resolve => setTimeout(resolve, 4000))
52
+ const ds = devServers.get(id)
53
+ return NextResponse.json({ running: !!ds, url: ds?.url || `http://${localIP()}:4321` })
54
+
55
+ } else if (action === 'stop') {
56
+ if (devServers.has(id)) {
57
+ const ds = devServers.get(id)!
58
+ try { ds.proc.kill('SIGTERM') } catch {}
59
+ try { process.kill(-ds.proc.pid, 'SIGTERM') } catch {}
60
+ devServers.delete(id)
61
+ }
62
+ return NextResponse.json({ running: false })
63
+
64
+ } else if (action === 'status') {
65
+ return NextResponse.json({ running: devServers.has(id), url: devServers.get(id)?.url })
66
+ }
67
+
68
+ return NextResponse.json({ running: false })
69
+ }
@@ -0,0 +1,70 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ackMailboxEnvelope, clearMailbox, listMailbox, sendMailboxEnvelope } from '@/lib/server/session-mailbox'
3
+ import { loadSessions } from '@/lib/server/storage'
4
+
5
+ function parseIntParam(value: string | null, fallback: number, min: number, max: number): number {
6
+ const parsed = value ? Number.parseInt(value, 10) : Number.NaN
7
+ if (!Number.isFinite(parsed)) return fallback
8
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
9
+ }
10
+
11
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
12
+ const { id } = await params
13
+ const { searchParams } = new URL(req.url)
14
+ const limit = parseIntParam(searchParams.get('limit'), 50, 1, 500)
15
+ const includeAcked = searchParams.get('includeAcked') === 'true'
16
+ try {
17
+ const envelopes = listMailbox(id, { limit, includeAcked })
18
+ return NextResponse.json({ sessionId: id, count: envelopes.length, envelopes })
19
+ } catch (err: any) {
20
+ return NextResponse.json({ error: err?.message || 'Failed to load mailbox.' }, { status: 404 })
21
+ }
22
+ }
23
+
24
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
25
+ const { id } = await params
26
+ const body = await req.json().catch(() => ({}))
27
+ const action = typeof body?.action === 'string' ? body.action : 'send'
28
+ const sessions = loadSessions()
29
+ const current = sessions[id]
30
+ if (!current) return NextResponse.json({ error: `Session not found: ${id}` }, { status: 404 })
31
+
32
+ try {
33
+ if (action === 'send') {
34
+ const toSessionId = typeof body?.toSessionId === 'string' ? body.toSessionId : ''
35
+ const payload = typeof body?.payload === 'string' ? body.payload : ''
36
+ const type = typeof body?.type === 'string' ? body.type : 'message'
37
+ if (!toSessionId) return NextResponse.json({ error: 'toSessionId is required for send.' }, { status: 400 })
38
+ if (!payload.trim()) return NextResponse.json({ error: 'payload is required for send.' }, { status: 400 })
39
+ const envelope = sendMailboxEnvelope({
40
+ toSessionId,
41
+ type,
42
+ payload,
43
+ fromSessionId: id,
44
+ fromAgentId: current.agentId || null,
45
+ correlationId: typeof body?.correlationId === 'string' ? body.correlationId : null,
46
+ ttlSec: typeof body?.ttlSec === 'number' ? body.ttlSec : null,
47
+ })
48
+ return NextResponse.json({ ok: true, envelope })
49
+ }
50
+
51
+ if (action === 'ack') {
52
+ const envelopeId = typeof body?.envelopeId === 'string' ? body.envelopeId : ''
53
+ if (!envelopeId) return NextResponse.json({ error: 'envelopeId is required for ack.' }, { status: 400 })
54
+ const envelope = ackMailboxEnvelope(id, envelopeId)
55
+ if (!envelope) return NextResponse.json({ error: `Envelope not found: ${envelopeId}` }, { status: 404 })
56
+ return NextResponse.json({ ok: true, envelope })
57
+ }
58
+
59
+ if (action === 'clear') {
60
+ const includeAcked = body?.includeAcked !== false
61
+ const result = clearMailbox(id, includeAcked)
62
+ return NextResponse.json({ ok: true, ...result })
63
+ }
64
+
65
+ return NextResponse.json({ error: `Unsupported action: ${action}` }, { status: 400 })
66
+ } catch (err: any) {
67
+ return NextResponse.json({ error: err?.message || 'Mailbox operation failed.' }, { status: 500 })
68
+ }
69
+ }
70
+
@@ -0,0 +1,94 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { enqueueSessionRun } from '@/lib/server/session-run-manager'
3
+ import { loadSessions } from '@/lib/server/storage'
4
+ import {
5
+ buildMainLoopHeartbeatPrompt,
6
+ getMainLoopStateForSession,
7
+ isMainSession,
8
+ pushMainLoopEventToMainSessions,
9
+ setMainLoopStateForSession,
10
+ } from '@/lib/server/main-agent-loop'
11
+
12
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params
14
+ const sessions = loadSessions()
15
+ const session = sessions[id]
16
+ if (!session) return new NextResponse('Session not found', { status: 404 })
17
+ if (!isMainSession(session)) return new NextResponse('Main-loop controls only apply to __main__ sessions', { status: 400 })
18
+ const state = getMainLoopStateForSession(id)
19
+ return NextResponse.json({ sessionId: id, state })
20
+ }
21
+
22
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
23
+ const { id } = await params
24
+ const body = await req.json().catch(() => ({}))
25
+ const action = typeof body.action === 'string' ? body.action.trim() : ''
26
+
27
+ const sessions = loadSessions()
28
+ const session = sessions[id]
29
+ if (!session) return new NextResponse('Session not found', { status: 404 })
30
+ if (!isMainSession(session)) return new NextResponse('Main-loop controls only apply to __main__ sessions', { status: 400 })
31
+
32
+ if (action === 'pause') {
33
+ const state = setMainLoopStateForSession(id, { paused: true })
34
+ return NextResponse.json({ ok: true, action, state })
35
+ }
36
+
37
+ if (action === 'resume') {
38
+ const state = setMainLoopStateForSession(id, { paused: false })
39
+ return NextResponse.json({ ok: true, action, state })
40
+ }
41
+
42
+ if (action === 'set_goal') {
43
+ const goal = typeof body.goal === 'string' ? body.goal.trim() : ''
44
+ if (!goal) return new NextResponse('goal is required for set_goal', { status: 400 })
45
+ const state = setMainLoopStateForSession(id, {
46
+ goal,
47
+ status: 'progress',
48
+ paused: false,
49
+ followupChainCount: 0,
50
+ })
51
+ pushMainLoopEventToMainSessions({
52
+ type: 'operator_goal',
53
+ text: `Operator set mission goal: ${goal}`,
54
+ user: session.user || null,
55
+ })
56
+ return NextResponse.json({ ok: true, action, state })
57
+ }
58
+
59
+ if (action === 'set_mode') {
60
+ const mode = body.mode === 'assist' ? 'assist' : body.mode === 'autonomous' ? 'autonomous' : null
61
+ if (!mode) return new NextResponse('mode must be "assist" or "autonomous"', { status: 400 })
62
+ const state = setMainLoopStateForSession(id, { autonomyMode: mode })
63
+ return NextResponse.json({ ok: true, action, state })
64
+ }
65
+
66
+ if (action === 'clear_events') {
67
+ const state = setMainLoopStateForSession(id, { pendingEvents: [] })
68
+ return NextResponse.json({ ok: true, action, state })
69
+ }
70
+
71
+ if (action === 'nudge') {
72
+ const state = getMainLoopStateForSession(id)
73
+ if (state?.paused) {
74
+ return new NextResponse('Mission loop is paused; resume first', { status: 409 })
75
+ }
76
+ const note = typeof body.note === 'string' ? body.note.trim() : ''
77
+ const prompt = buildMainLoopHeartbeatPrompt(
78
+ session,
79
+ 'Operator requested manual nudge: execute one concrete next step now and report concise progress.',
80
+ )
81
+ const message = note ? `${prompt}\nOperator note: ${note.slice(0, 500)}` : prompt
82
+ const run = enqueueSessionRun({
83
+ sessionId: id,
84
+ message,
85
+ internal: true,
86
+ source: 'mission-control',
87
+ mode: 'collect',
88
+ dedupeKey: `mission-control:nudge:${id}`,
89
+ })
90
+ return NextResponse.json({ ok: true, action, runId: run.runId, position: run.position, deduped: run.deduped || false, state })
91
+ }
92
+
93
+ return new NextResponse('Unknown action. Use pause, resume, set_goal, set_mode, clear_events, or nudge.', { status: 400 })
94
+ }