@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,250 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
4
+ import { NextResponse } from 'next/server'
5
+ import { loadAgents, loadCredentials, loadSettings } from '@/lib/server/storage'
6
+
7
+ type CheckStatus = 'pass' | 'warn' | 'fail'
8
+
9
+ interface SetupCheck {
10
+ id: string
11
+ label: string
12
+ status: CheckStatus
13
+ detail: string
14
+ required?: boolean
15
+ }
16
+
17
+ interface CommandResult {
18
+ ok: boolean
19
+ output: string
20
+ error?: string
21
+ }
22
+
23
+ const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
24
+
25
+ function run(command: string, args: string[], timeoutMs = 8_000): CommandResult {
26
+ try {
27
+ const result = spawnSync(command, args, {
28
+ cwd: process.cwd(),
29
+ encoding: 'utf8',
30
+ timeout: timeoutMs,
31
+ })
32
+ if (result.error) {
33
+ return { ok: false, output: '', error: result.error.message }
34
+ }
35
+ if (typeof result.status === 'number' && result.status !== 0) {
36
+ const err = (result.stderr || result.stdout || `exit ${result.status}`).trim()
37
+ return { ok: false, output: '', error: err || `exit ${result.status}` }
38
+ }
39
+ return { ok: true, output: (result.stdout || '').trim() }
40
+ } catch (err: any) {
41
+ return { ok: false, output: '', error: err?.message || String(err) }
42
+ }
43
+ }
44
+
45
+ function getLatestStableTag(): string | null {
46
+ const listed = run('git', ['tag', '--list', 'v*', '--sort=-v:refname'], 4_000)
47
+ if (!listed.ok) return null
48
+ const tags = listed.output
49
+ .split('\n')
50
+ .map((line) => line.trim())
51
+ .filter(Boolean)
52
+ return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
53
+ }
54
+
55
+ function commandExists(name: string): boolean {
56
+ const lookup = process.platform === 'win32' ? 'where' : 'which'
57
+ return run(lookup, [name], 3_000).ok
58
+ }
59
+
60
+ function pushCheck(
61
+ checks: SetupCheck[],
62
+ id: string,
63
+ label: string,
64
+ status: CheckStatus,
65
+ detail: string,
66
+ required = false,
67
+ ) {
68
+ checks.push({ id, label, status, detail, required })
69
+ }
70
+
71
+ function testDataWriteAccess(dataDir: string): { ok: boolean; error?: string } {
72
+ try {
73
+ fs.mkdirSync(dataDir, { recursive: true })
74
+ const probe = path.join(dataDir, `.doctor-write-${Date.now()}.tmp`)
75
+ fs.writeFileSync(probe, 'ok', 'utf8')
76
+ fs.unlinkSync(probe)
77
+ return { ok: true }
78
+ } catch (err: any) {
79
+ return { ok: false, error: err?.message || String(err) }
80
+ }
81
+ }
82
+
83
+ export async function GET(req: Request) {
84
+ const url = new URL(req.url)
85
+ const includeRemote = url.searchParams.get('remote') === '1'
86
+ const checks: SetupCheck[] = []
87
+ const actions: string[] = []
88
+ const checkedAt = Date.now()
89
+
90
+ const nodeVersion = process.versions.node
91
+ const nodeMajor = Number.parseInt(String(nodeVersion).split('.')[0] || '0', 10)
92
+ if (nodeMajor >= 20) {
93
+ pushCheck(checks, 'node-version', 'Node.js version', 'pass', `Detected Node ${nodeVersion}.`, true)
94
+ } else {
95
+ pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 20+ is required.`, true)
96
+ actions.push('Install Node.js 20 or newer from https://nodejs.org and rerun setup.')
97
+ }
98
+
99
+ const npmCheck = run('npm', ['--version'], 5_000)
100
+ if (npmCheck.ok) {
101
+ pushCheck(checks, 'npm', 'npm availability', 'pass', `npm ${npmCheck.output} is available.`, true)
102
+ } else {
103
+ pushCheck(checks, 'npm', 'npm availability', 'fail', npmCheck.error || 'npm was not found in PATH.', true)
104
+ actions.push('Install npm and rerun `npm run setup:easy`.')
105
+ }
106
+
107
+ const dataDir = path.join(process.cwd(), 'data')
108
+ const dataWrite = testDataWriteAccess(dataDir)
109
+ if (dataWrite.ok) {
110
+ pushCheck(checks, 'data-dir', 'Data directory permissions', 'pass', `Writable: ${dataDir}`, true)
111
+ } else {
112
+ pushCheck(checks, 'data-dir', 'Data directory permissions', 'fail', dataWrite.error || `Cannot write to ${dataDir}`, true)
113
+ actions.push(`Fix filesystem permissions for ${dataDir}.`)
114
+ }
115
+
116
+ const envFile = path.join(process.cwd(), '.env.local')
117
+ if (fs.existsSync(envFile)) {
118
+ pushCheck(checks, 'env-file', '.env.local', 'pass', '.env.local is present.')
119
+ } else {
120
+ pushCheck(checks, 'env-file', '.env.local', 'warn', '.env.local was not found yet. It will be created automatically on first run.')
121
+ actions.push('Run `npm run dev` once to auto-generate ACCESS_KEY and CREDENTIAL_SECRET.')
122
+ }
123
+
124
+ const hasAccessKey = !!process.env.ACCESS_KEY?.trim()
125
+ if (hasAccessKey) {
126
+ pushCheck(checks, 'access-key', 'Access key', 'pass', 'ACCESS_KEY is configured.', true)
127
+ } else {
128
+ pushCheck(checks, 'access-key', 'Access key', 'fail', 'ACCESS_KEY is missing.', true)
129
+ actions.push('Start the app once so SwarmClaw can generate ACCESS_KEY automatically.')
130
+ }
131
+
132
+ const hasCredentialSecret = !!process.env.CREDENTIAL_SECRET?.trim()
133
+ if (hasCredentialSecret) {
134
+ pushCheck(checks, 'credential-secret', 'Credential secret', 'pass', 'CREDENTIAL_SECRET is configured.', true)
135
+ } else {
136
+ pushCheck(checks, 'credential-secret', 'Credential secret', 'fail', 'CREDENTIAL_SECRET is missing.', true)
137
+ actions.push('Start the app once so SwarmClaw can generate CREDENTIAL_SECRET automatically.')
138
+ }
139
+
140
+ const settings = loadSettings()
141
+ if (settings?.setupCompleted === true) {
142
+ pushCheck(checks, 'setup-wizard', 'Setup wizard', 'pass', 'Initial setup has been completed.')
143
+ } else {
144
+ pushCheck(checks, 'setup-wizard', 'Setup wizard', 'warn', 'Initial setup is not marked complete yet.')
145
+ actions.push('Open the UI and finish the setup wizard at least once.')
146
+ }
147
+
148
+ const agents = Object.values(loadAgents() || {})
149
+ if (agents.length > 0) {
150
+ pushCheck(checks, 'agents', 'Agents', 'pass', `${agents.length} agent(s) configured.`)
151
+ } else {
152
+ pushCheck(checks, 'agents', 'Agents', 'warn', 'No agents found.')
153
+ actions.push('Create a starter agent from the setup wizard.')
154
+ }
155
+
156
+ const credentials = Object.values(loadCredentials() || {})
157
+ if (credentials.length > 0) {
158
+ pushCheck(checks, 'credentials', 'Credentials', 'pass', `${credentials.length} credential(s) saved.`)
159
+ } else {
160
+ pushCheck(checks, 'credentials', 'Credentials', 'warn', 'No API credentials saved (OK for local-only Ollama).')
161
+ actions.push('If using cloud providers, add an API key in the setup wizard or Settings → Providers.')
162
+ }
163
+
164
+ const optionalBinaries: Array<{ id: string; label: string; command: string }> = [
165
+ { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
166
+ { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
167
+ { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
168
+ ]
169
+
170
+ for (const binary of optionalBinaries) {
171
+ const exists = commandExists(binary.command)
172
+ pushCheck(
173
+ checks,
174
+ binary.id,
175
+ binary.label,
176
+ exists ? 'pass' : 'warn',
177
+ exists
178
+ ? `${binary.command} is installed.`
179
+ : `${binary.command} is not installed (optional, only needed for ${binary.label} provider).`,
180
+ )
181
+ }
182
+
183
+ const gitRootCheck = run('git', ['rev-parse', '--is-inside-work-tree'], 4_000)
184
+ let localSha: string | null = null
185
+ let remoteSha: string | null = null
186
+ let behindBy = 0
187
+ let workingTreeDirty = false
188
+
189
+ if (!gitRootCheck.ok) {
190
+ pushCheck(checks, 'git-repo', 'Git repository', 'warn', 'This directory is not a git repository. Auto-update checks are disabled.')
191
+ } else {
192
+ pushCheck(checks, 'git-repo', 'Git repository', 'pass', 'Git repository detected.')
193
+
194
+ localSha = run('git', ['rev-parse', '--short', 'HEAD'], 4_000).output || null
195
+ const dirty = run('git', ['status', '--porcelain'], 4_000)
196
+ workingTreeDirty = !!dirty.output
197
+ if (workingTreeDirty) {
198
+ pushCheck(checks, 'git-dirty', 'Working tree cleanliness', 'warn', 'Uncommitted local changes detected.')
199
+ actions.push('Commit or stash local changes before running automatic updates.')
200
+ } else {
201
+ pushCheck(checks, 'git-dirty', 'Working tree cleanliness', 'pass', 'Working tree is clean.')
202
+ }
203
+
204
+ if (includeRemote) {
205
+ const fetch = run('git', ['fetch', '--tags', 'origin', '--quiet'], 12_000)
206
+ if (!fetch.ok) {
207
+ pushCheck(checks, 'git-remote', 'Remote update check', 'warn', fetch.error || 'Could not check remote release tags.')
208
+ } else {
209
+ const latestTag = getLatestStableTag()
210
+ if (!latestTag) {
211
+ pushCheck(checks, 'git-update', 'Update availability', 'warn', 'No stable release tags found yet; updater will fallback to main.')
212
+ } else {
213
+ const behind = run('git', ['rev-list', `HEAD..${latestTag}^{commit}`, '--count'], 4_000)
214
+ behindBy = Number.parseInt(behind.output || '0', 10) || 0
215
+ remoteSha = run('git', ['rev-parse', '--short', `${latestTag}^{commit}`], 4_000).output || localSha
216
+
217
+ if (behindBy > 0) {
218
+ pushCheck(checks, 'git-update', 'Update availability', 'warn', `${behindBy} commit(s) available to stable release ${latestTag}.`)
219
+ actions.push('Run `npm run update:easy` or use the in-app update banner.')
220
+ } else {
221
+ pushCheck(checks, 'git-update', 'Update availability', 'pass', `Already on stable release ${latestTag} or newer.`)
222
+ }
223
+ }
224
+ }
225
+ } else {
226
+ pushCheck(checks, 'git-remote', 'Remote update check', 'warn', 'Skipped (pass ?remote=1 to include remote stable-tag check).')
227
+ }
228
+ }
229
+
230
+ const failedRequired = checks.filter((c) => c.required && c.status === 'fail').length
231
+ const warnings = checks.filter((c) => c.status === 'warn').length
232
+ const ok = failedRequired === 0
233
+ const summary = ok
234
+ ? (warnings > 0 ? `Setup mostly healthy with ${warnings} warning(s).` : 'Setup looks healthy.')
235
+ : `Setup has ${failedRequired} required failure(s).`
236
+
237
+ return NextResponse.json({
238
+ ok,
239
+ checkedAt,
240
+ summary,
241
+ checks,
242
+ actions: Array.from(new Set(actions)),
243
+ git: {
244
+ localSha,
245
+ remoteSha,
246
+ behindBy,
247
+ dirty: workingTreeDirty,
248
+ },
249
+ })
250
+ }
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSkills, saveSkills, deleteSkill } from '@/lib/server/storage'
3
+ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
4
+
5
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const skills = loadSkills()
8
+ if (!skills[id]) return new NextResponse(null, { status: 404 })
9
+ return NextResponse.json(skills[id])
10
+ }
11
+
12
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params
14
+ const body = await req.json()
15
+ const skills = loadSkills()
16
+ if (!skills[id]) return new NextResponse(null, { status: 404 })
17
+ const normalized = normalizeSkillPayload({ ...skills[id], ...body })
18
+ skills[id] = {
19
+ ...skills[id],
20
+ ...body,
21
+ name: normalized.name,
22
+ filename: normalized.filename,
23
+ description: normalized.description,
24
+ content: normalized.content,
25
+ sourceUrl: normalized.sourceUrl,
26
+ sourceFormat: normalized.sourceFormat,
27
+ id,
28
+ updatedAt: Date.now(),
29
+ }
30
+ saveSkills(skills)
31
+ return NextResponse.json(skills[id])
32
+ }
33
+
34
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
35
+ const { id } = await params
36
+ const skills = loadSkills()
37
+ if (!skills[id]) return new NextResponse(null, { status: 404 })
38
+ deleteSkill(id)
39
+ return NextResponse.json({ deleted: id })
40
+ }
@@ -0,0 +1,69 @@
1
+ import crypto from 'crypto'
2
+ import { NextResponse } from 'next/server'
3
+ import { loadSkills, saveSkills } from '@/lib/server/storage'
4
+ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
+
6
+ const MAX_SKILL_BYTES = 2 * 1024 * 1024
7
+
8
+ function validateHttpUrl(value: unknown): string {
9
+ if (typeof value !== 'string' || !value.trim()) {
10
+ throw new Error('url is required')
11
+ }
12
+ const parsed = new URL(value.trim())
13
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
14
+ throw new Error('Only http/https URLs are supported')
15
+ }
16
+ return parsed.toString()
17
+ }
18
+
19
+ export async function POST(req: Request) {
20
+ try {
21
+ const body = await req.json()
22
+ const url = validateHttpUrl(body.url)
23
+
24
+ const res = await fetch(url, {
25
+ signal: AbortSignal.timeout(20_000),
26
+ headers: {
27
+ 'User-Agent': 'SwarmClaw/1.0 skill-import',
28
+ },
29
+ })
30
+
31
+ if (!res.ok) {
32
+ throw new Error(`Failed to fetch skill (${res.status})`)
33
+ }
34
+
35
+ const content = await res.text()
36
+ if (!content.trim()) {
37
+ throw new Error('Fetched skill file is empty')
38
+ }
39
+ if (Buffer.byteLength(content, 'utf8') > MAX_SKILL_BYTES) {
40
+ throw new Error('Skill file too large (max 2MB)')
41
+ }
42
+
43
+ const normalized = normalizeSkillPayload({
44
+ ...body,
45
+ content,
46
+ sourceUrl: url,
47
+ })
48
+
49
+ const skills = loadSkills()
50
+ const id = crypto.randomBytes(4).toString('hex')
51
+ skills[id] = {
52
+ id,
53
+ name: normalized.name,
54
+ filename: normalized.filename,
55
+ description: normalized.description,
56
+ content: normalized.content,
57
+ sourceUrl: normalized.sourceUrl,
58
+ sourceFormat: normalized.sourceFormat,
59
+ createdAt: Date.now(),
60
+ updatedAt: Date.now(),
61
+ }
62
+ saveSkills(skills)
63
+
64
+ return NextResponse.json(skills[id])
65
+ } catch (err: unknown) {
66
+ const message = err instanceof Error ? err.message : 'Failed to import skill'
67
+ return NextResponse.json({ error: message }, { status: 400 })
68
+ }
69
+ }
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadSkills, saveSkills } from '@/lib/server/storage'
4
+ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
+
6
+ export async function GET() {
7
+ return NextResponse.json(loadSkills())
8
+ }
9
+
10
+ export async function POST(req: Request) {
11
+ const body = await req.json()
12
+ const skills = loadSkills()
13
+ const id = crypto.randomBytes(4).toString('hex')
14
+ const normalized = normalizeSkillPayload(body)
15
+ skills[id] = {
16
+ id,
17
+ name: normalized.name,
18
+ filename: normalized.filename || `skill-${id}.md`,
19
+ content: normalized.content || '',
20
+ description: normalized.description || '',
21
+ sourceUrl: normalized.sourceUrl,
22
+ sourceFormat: normalized.sourceFormat,
23
+ createdAt: Date.now(),
24
+ updatedAt: Date.now(),
25
+ }
26
+ saveSkills(skills)
27
+ return NextResponse.json(skills[id])
28
+ }
@@ -0,0 +1,102 @@
1
+ import crypto from 'crypto'
2
+ import { NextResponse } from 'next/server'
3
+ import { loadTasks, saveTasks } from '@/lib/server/storage'
4
+ import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
+ import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
+ import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
+
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ // Keep completed queue integrity even if daemon is not running.
11
+ validateCompletedTasksQueue()
12
+
13
+ const { id } = await params
14
+ const tasks = loadTasks()
15
+ if (!tasks[id]) return new NextResponse(null, { status: 404 })
16
+ return NextResponse.json(tasks[id])
17
+ }
18
+
19
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
20
+ const { id } = await params
21
+ const body = await req.json()
22
+ const tasks = loadTasks()
23
+ if (!tasks[id]) return new NextResponse(null, { status: 404 })
24
+
25
+ const prevStatus = tasks[id].status
26
+
27
+ // Support atomic comment append to avoid race conditions
28
+ if (body.appendComment) {
29
+ if (!tasks[id].comments) tasks[id].comments = []
30
+ tasks[id].comments.push(body.appendComment)
31
+ tasks[id].updatedAt = Date.now()
32
+ } else {
33
+ Object.assign(tasks[id], body, { updatedAt: Date.now() })
34
+ }
35
+ tasks[id].id = id // prevent id overwrite
36
+
37
+ // Set archivedAt when transitioning to archived
38
+ if (prevStatus !== 'archived' && tasks[id].status === 'archived') {
39
+ tasks[id].archivedAt = Date.now()
40
+ }
41
+
42
+ // Re-validate any completed task updates so "completed" always means actually done.
43
+ if (tasks[id].status === 'completed') {
44
+ const report = ensureTaskCompletionReport(tasks[id])
45
+ if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
46
+ const validation = validateTaskCompletion(tasks[id], { report })
47
+ tasks[id].validation = validation
48
+ if (validation.ok) {
49
+ tasks[id].completedAt = tasks[id].completedAt || Date.now()
50
+ tasks[id].error = null
51
+ } else {
52
+ tasks[id].status = 'failed'
53
+ tasks[id].completedAt = null
54
+ tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
55
+ if (!tasks[id].comments) tasks[id].comments = []
56
+ tasks[id].comments.push({
57
+ id: crypto.randomBytes(4).toString('hex'),
58
+ author: 'System',
59
+ text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
60
+ createdAt: Date.now(),
61
+ })
62
+ }
63
+ }
64
+
65
+ saveTasks(tasks)
66
+ if (prevStatus !== tasks[id].status) {
67
+ pushMainLoopEventToMainSessions({
68
+ type: 'task_status_changed',
69
+ text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
70
+ })
71
+ }
72
+
73
+ // If task is manually transitioned to a terminal status, disable session heartbeat.
74
+ if (prevStatus !== tasks[id].status && (tasks[id].status === 'completed' || tasks[id].status === 'failed')) {
75
+ disableSessionHeartbeat(tasks[id].sessionId)
76
+ }
77
+
78
+ // If status changed to 'queued', enqueue it
79
+ if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
80
+ enqueueTask(id)
81
+ }
82
+
83
+ return NextResponse.json(tasks[id])
84
+ }
85
+
86
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
87
+ const { id } = await params
88
+ const tasks = loadTasks()
89
+ if (!tasks[id]) return new NextResponse(null, { status: 404 })
90
+
91
+ // Soft delete: move to archived status instead of hard delete
92
+ tasks[id].status = 'archived'
93
+ tasks[id].archivedAt = Date.now()
94
+ tasks[id].updatedAt = Date.now()
95
+ saveTasks(tasks)
96
+ pushMainLoopEventToMainSessions({
97
+ type: 'task_archived',
98
+ text: `Task archived: "${tasks[id].title}" (${id}).`,
99
+ })
100
+
101
+ return NextResponse.json(tasks[id])
102
+ }
@@ -0,0 +1,115 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadTasks, saveTasks, loadSettings } from '@/lib/server/storage'
4
+ import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
+ import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
+ import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
+
9
+ export async function GET(req: Request) {
10
+ // Keep completed queue integrity even if daemon is not running.
11
+ validateCompletedTasksQueue()
12
+
13
+ const { searchParams } = new URL(req.url)
14
+ const includeArchived = searchParams.get('includeArchived') === 'true'
15
+ const allTasks = loadTasks()
16
+
17
+ if (includeArchived) {
18
+ return NextResponse.json(allTasks)
19
+ }
20
+
21
+ // Exclude archived tasks by default
22
+ const filtered: Record<string, typeof allTasks[string]> = {}
23
+ for (const [id, task] of Object.entries(allTasks)) {
24
+ if (task.status !== 'archived') {
25
+ filtered[id] = task
26
+ }
27
+ }
28
+ return NextResponse.json(filtered)
29
+ }
30
+
31
+ export async function DELETE(req: Request) {
32
+ const { searchParams } = new URL(req.url)
33
+ const filter = searchParams.get('filter') // 'all' | 'schedule' | 'done' | null
34
+ const tasks = loadTasks()
35
+ let removed = 0
36
+
37
+ const shouldRemove = (task: { status: string; sourceType?: string }) =>
38
+ filter === 'all' ||
39
+ (filter === 'schedule' && task.sourceType === 'schedule') ||
40
+ (filter === 'done' && (task.status === 'completed' || task.status === 'failed')) ||
41
+ (!filter && task.status === 'archived')
42
+
43
+ const { deleteTask } = await import('@/lib/server/storage')
44
+ for (const [id, task] of Object.entries(tasks)) {
45
+ if (shouldRemove(task as { status: string; sourceType?: string })) {
46
+ deleteTask(id)
47
+ removed++
48
+ }
49
+ }
50
+ return NextResponse.json({ removed, remaining: Object.keys(tasks).length - removed })
51
+ }
52
+
53
+ export async function POST(req: Request) {
54
+ const body = await req.json()
55
+ const id = crypto.randomBytes(4).toString('hex')
56
+ const now = Date.now()
57
+ const tasks = loadTasks()
58
+ const settings = loadSettings()
59
+ const maxAttempts = Number.isFinite(Number(body.maxAttempts))
60
+ ? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
61
+ : Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
62
+ const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
63
+ ? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
64
+ : Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
65
+ tasks[id] = {
66
+ id,
67
+ title: body.title || 'Untitled Task',
68
+ description: body.description || '',
69
+ status: body.status || 'backlog',
70
+ agentId: body.agentId || '',
71
+ goalContract: body.goalContract || null,
72
+ cwd: typeof body.cwd === 'string' ? body.cwd : null,
73
+ file: typeof body.file === 'string' ? body.file : null,
74
+ sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
75
+ result: typeof body.result === 'string' ? body.result : null,
76
+ error: typeof body.error === 'string' ? body.error : null,
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ queuedAt: null,
80
+ startedAt: null,
81
+ completedAt: null,
82
+ archivedAt: null,
83
+ attempts: 0,
84
+ maxAttempts,
85
+ retryBackoffSec,
86
+ retryScheduledAt: null,
87
+ deadLetteredAt: null,
88
+ checkpoint: null,
89
+ }
90
+
91
+ if (tasks[id].status === 'completed') {
92
+ const report = ensureTaskCompletionReport(tasks[id])
93
+ if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
94
+ const validation = validateTaskCompletion(tasks[id], { report })
95
+ tasks[id].validation = validation
96
+ if (validation.ok) {
97
+ tasks[id].completedAt = Date.now()
98
+ tasks[id].error = null
99
+ } else {
100
+ tasks[id].status = 'failed'
101
+ tasks[id].completedAt = null
102
+ tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
103
+ }
104
+ }
105
+
106
+ saveTasks(tasks)
107
+ pushMainLoopEventToMainSessions({
108
+ type: 'task_created',
109
+ text: `Task created: "${tasks[id].title}" (${id}) with status ${tasks[id].status}.`,
110
+ })
111
+ if (tasks[id].status === 'queued') {
112
+ enqueueTask(id)
113
+ }
114
+ return NextResponse.json(tasks[id])
115
+ }
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSettings } from '@/lib/server/storage'
3
+
4
+ export async function POST(req: Request) {
5
+ const settings = loadSettings()
6
+ const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
7
+ const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
8
+
9
+ if (!ELEVENLABS_KEY) {
10
+ return new NextResponse('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
11
+ }
12
+
13
+ const { text } = await req.json()
14
+ const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}`, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'xi-api-key': ELEVENLABS_KEY,
18
+ 'Content-Type': 'application/json',
19
+ 'Accept': 'audio/mpeg',
20
+ },
21
+ body: JSON.stringify({
22
+ text,
23
+ model_id: 'eleven_multilingual_v2',
24
+ voice_settings: { stability: 0.5, similarity_boost: 0.75 },
25
+ }),
26
+ })
27
+
28
+ if (!apiRes.ok) {
29
+ const err = await apiRes.text()
30
+ return new NextResponse(err, { status: apiRes.status })
31
+ }
32
+
33
+ const audioBuffer = await apiRes.arrayBuffer()
34
+ return new NextResponse(audioBuffer, {
35
+ headers: {
36
+ 'Content-Type': 'audio/mpeg',
37
+ 'Cache-Control': 'no-cache',
38
+ },
39
+ })
40
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import crypto from 'crypto'
5
+ import { UPLOAD_DIR } from '@/lib/server/storage'
6
+
7
+ export async function POST(req: Request) {
8
+ const filename = req.headers.get('x-filename') || 'image.png'
9
+ const buf = Buffer.from(await req.arrayBuffer())
10
+ const name = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
11
+ const filePath = path.join(UPLOAD_DIR, name)
12
+
13
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
14
+ fs.writeFileSync(filePath, buf)
15
+ console.log(`[upload] saved ${buf.length} bytes to ${filePath}`)
16
+
17
+ return NextResponse.json({ path: filePath, size: buf.length, url: `/api/uploads/${name}` })
18
+ }