@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -4,6 +4,7 @@ import http from 'http'
4
4
  import fs from 'fs'
5
5
  import path from 'path'
6
6
  import { localIP } from '@/lib/server/storage'
7
+ import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // MIME types for static server
@@ -77,6 +78,13 @@ interface ProjectInfo {
77
78
  framework?: string // e.g. 'vite', 'next', 'cra'
78
79
  }
79
80
 
81
+ function buildFrameworkArgs(framework: string | undefined, port: number): string[] {
82
+ if (framework === 'next') {
83
+ return ['--', '--hostname', '0.0.0.0', '--port', String(port)]
84
+ }
85
+ return ['--', '--port', String(port), '--host', '0.0.0.0']
86
+ }
87
+
80
88
  function detectProject(dir: string): ProjectInfo {
81
89
  const pkgPath = path.join(dir, 'package.json')
82
90
  if (!fs.existsSync(pkgPath)) {
@@ -168,7 +176,7 @@ function createStaticServer(dir: string): http.Server {
168
176
  // npm dev server
169
177
  // ---------------------------------------------------------------------------
170
178
 
171
- async function startNpmServer(dir: string, command: string[], port: number): Promise<PreviewServer> {
179
+ async function startNpmServer(dir: string, command: string[], port: number, framework?: string): Promise<PreviewServer> {
172
180
  // Install deps if node_modules missing
173
181
  if (!fs.existsSync(path.join(dir, 'node_modules'))) {
174
182
  console.log(`[preview] Installing dependencies in ${dir}`)
@@ -190,7 +198,7 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
190
198
  const args = [...command.slice(1)]
191
199
  const cmdName = command[0]
192
200
 
193
- const proc = spawn(cmdName, [...args, '--', '--port', String(port), '--host', '0.0.0.0'], {
201
+ const proc = spawn(cmdName, [...args, ...buildFrameworkArgs(framework, port)], {
194
202
  cwd: dir,
195
203
  stdio: ['ignore', 'pipe', 'pipe'],
196
204
  env,
@@ -234,6 +242,10 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
234
242
 
235
243
  // Wait for the server to start and detect the actual port
236
244
  await new Promise((resolve) => setTimeout(resolve, 5000))
245
+ if (proc.exitCode !== null) {
246
+ servers.delete(dirKey(dir))
247
+ throw new Error(`npm dev server exited early with code ${proc.exitCode}\n${log.slice(-4000)}`)
248
+ }
237
249
  entry.port = detectedPort
238
250
  entry.log = log
239
251
 
@@ -263,7 +275,8 @@ export async function POST(req: Request) {
263
275
  }
264
276
 
265
277
  const dir = resolveServeDir(filePath)
266
- const key = dirKey(dir)
278
+ const launch = resolveDevServerLaunchDir(dir)
279
+ const key = dirKey(launch.launchDir)
267
280
 
268
281
  if (action === 'start') {
269
282
  if (servers.has(key)) {
@@ -274,16 +287,18 @@ export async function POST(req: Request) {
274
287
  return NextResponse.json({ error: 'Directory not found' }, { status: 404 })
275
288
  }
276
289
 
277
- const project = detectProject(dir)
290
+ const project = detectProject(launch.launchDir)
278
291
  const port = await findFreePort()
279
292
 
280
293
  if (project.type === 'npm' && project.devCommand) {
281
- console.log(`[preview] Detected ${project.framework} project in ${dir}, running: ${project.devCommand.join(' ')}`)
294
+ console.log(`[preview] Detected ${project.framework} project in ${launch.launchDir}, running: ${project.devCommand.join(' ')}`)
282
295
  try {
283
- const entry = await startNpmServer(dir, project.devCommand, port)
296
+ const entry = await startNpmServer(launch.launchDir, project.devCommand, port, project.framework)
284
297
  return NextResponse.json({
285
298
  ...buildResponse(entry),
286
299
  framework: project.framework,
300
+ inputDir: dir,
301
+ launchDir: launch.launchDir,
287
302
  })
288
303
  } catch (err: unknown) {
289
304
  console.error(`[preview] npm server failed, falling back to static:`, err)
@@ -313,15 +328,15 @@ export async function POST(req: Request) {
313
328
  }
314
329
  if (srv.server) srv.server.close()
315
330
  servers.delete(key)
316
- console.log(`[preview] Stopped server for ${dir}`)
331
+ console.log(`[preview] Stopped server for ${launch.launchDir}`)
317
332
  }
318
- return NextResponse.json({ running: false, dir })
333
+ return NextResponse.json({ running: false, dir: launch.launchDir })
319
334
 
320
335
  } else if (action === 'status') {
321
336
  if (servers.has(key)) {
322
337
  return NextResponse.json(buildResponse(servers.get(key)!))
323
338
  }
324
- return NextResponse.json({ running: false, dir })
339
+ return NextResponse.json({ running: false, dir: launch.launchDir })
325
340
 
326
341
  } else if (action === 'list') {
327
342
  const list = Array.from(servers.values()).map((s) => ({
@@ -331,8 +346,8 @@ export async function POST(req: Request) {
331
346
  return NextResponse.json({ servers: list })
332
347
 
333
348
  } else if (action === 'detect') {
334
- const project = detectProject(dir)
335
- return NextResponse.json({ dir, ...project })
349
+ const project = detectProject(launch.launchDir)
350
+ return NextResponse.json({ dir, launchDir: launch.launchDir, frameworkHint: launch.framework, ...project })
336
351
  }
337
352
 
338
353
  return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills } from '@/lib/server/storage'
2
+ import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills, loadSecrets, saveSecrets } from '@/lib/server/storage'
3
3
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
 
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -17,12 +18,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
17
18
  const { id } = await params
18
19
  const body = await req.json()
19
20
  const result = mutateItem(ops, id, (project) => {
20
- Object.assign(project, body, { updatedAt: Date.now() })
21
+ const patch = normalizeProjectPatchInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
22
+ Object.assign(project, patch, { updatedAt: Date.now() })
21
23
  delete (project as Record<string, unknown>).id
22
24
  project.id = id
23
25
  return project
24
26
  })
25
27
  if (!result) return notFound()
28
+ ensureProjectWorkspace(id, result.name)
26
29
  return NextResponse.json(result)
27
30
  }
28
31
 
@@ -50,6 +53,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
50
53
  clearProjectId(loadTasks, saveTasks, 'tasks')
51
54
  clearProjectId(loadSchedules, saveSchedules, 'schedules')
52
55
  clearProjectId(loadSkills, saveSkills, 'skills')
56
+ clearProjectId(loadSecrets, saveSecrets, 'secrets')
53
57
 
54
58
  return NextResponse.json({ ok: true })
55
59
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadProjects, saveProjects } from '@/lib/server/storage'
4
+ import { ensureProjectWorkspace, normalizeProjectCreateInput } from '@/lib/server/project-utils'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
  export const dynamic = 'force-dynamic'
6
7
 
@@ -13,15 +14,15 @@ export async function POST(req: Request) {
13
14
  const id = genId()
14
15
  const now = Date.now()
15
16
  const projects = loadProjects()
17
+ const normalized = normalizeProjectCreateInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
16
18
  projects[id] = {
17
19
  id,
18
- name: body.name || 'Unnamed Project',
19
- description: body.description || '',
20
- color: body.color || undefined,
20
+ ...normalized,
21
21
  createdAt: now,
22
22
  updatedAt: now,
23
23
  }
24
24
  saveProjects(projects)
25
+ ensureProjectWorkspace(id, projects[id].name)
25
26
  notify('projects')
26
27
  return NextResponse.json(projects[id])
27
28
  }
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
5
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
5
6
  import { enqueueTask } from '@/lib/server/queue'
6
7
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
7
8
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
@@ -20,6 +21,9 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
20
21
  const agents = loadAgents()
21
22
  const agent = agents[schedule.agentId]
22
23
  if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
24
+ if (isAgentDisabled(agent)) {
25
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'run schedules') }, { status: 409 })
26
+ }
23
27
 
24
28
  const tasks = loadTasks()
25
29
  const scheduleSignature = getScheduleSignatureKey(schedule)
@@ -0,0 +1,86 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { POST as createSchedule } from './route'
5
+ import { POST as runSchedule } from './[id]/run/route'
6
+ import { loadAgents, loadSchedules, saveAgents, saveSchedules } from '@/lib/server/storage'
7
+
8
+ const originalAgents = loadAgents()
9
+ const originalSchedules = loadSchedules()
10
+
11
+ function routeParams(id: string) {
12
+ return { params: Promise.resolve({ id }) }
13
+ }
14
+
15
+ function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
16
+ const agents = loadAgents()
17
+ const now = Date.now()
18
+ agents[id] = {
19
+ id,
20
+ name: 'Schedule Test Agent',
21
+ description: 'Schedule smoke test agent',
22
+ systemPrompt: 'Handle schedules.',
23
+ provider: 'openai',
24
+ model: 'gpt-4o-mini',
25
+ credentialId: null,
26
+ fallbackCredentialIds: [],
27
+ apiEndpoint: null,
28
+ plugins: ['manage_schedules'],
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ ...overrides,
32
+ }
33
+ saveAgents(agents)
34
+ }
35
+
36
+ afterEach(() => {
37
+ saveAgents(originalAgents)
38
+ saveSchedules(originalSchedules)
39
+ })
40
+
41
+ test('POST /api/schedules rejects disabled agents', async () => {
42
+ seedAgent('schedule-disabled-agent', { disabled: true })
43
+
44
+ const response = await createSchedule(new Request('http://local/api/schedules', {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify({
48
+ agentId: 'schedule-disabled-agent',
49
+ name: 'Disabled smoke',
50
+ taskPrompt: 'Send a reminder',
51
+ scheduleType: 'once',
52
+ runAt: Date.now() + 60_000,
53
+ status: 'active',
54
+ }),
55
+ }))
56
+
57
+ assert.equal(response.status, 409)
58
+ const payload = await response.json() as Record<string, unknown>
59
+ assert.match(String(payload.error || ''), /disabled/i)
60
+ })
61
+
62
+ test('POST /api/schedules/[id]/run rejects disabled agents', async () => {
63
+ seedAgent('schedule-run-disabled-agent', { disabled: true })
64
+ const schedules = loadSchedules()
65
+ schedules['schedule-disabled-run'] = {
66
+ id: 'schedule-disabled-run',
67
+ name: 'Disabled Run',
68
+ agentId: 'schedule-run-disabled-agent',
69
+ taskPrompt: 'Send a reminder',
70
+ scheduleType: 'once',
71
+ runAt: Date.now() + 60_000,
72
+ status: 'active',
73
+ createdAt: Date.now(),
74
+ updatedAt: Date.now(),
75
+ }
76
+ saveSchedules(schedules)
77
+
78
+ const response = await runSchedule(
79
+ new Request('http://local/api/schedules/schedule-disabled-run/run', { method: 'POST' }),
80
+ routeParams('schedule-disabled-run'),
81
+ )
82
+
83
+ assert.equal(response.status, 409)
84
+ const payload = await response.json() as Record<string, unknown>
85
+ assert.match(String(payload.error || ''), /disabled/i)
86
+ })
@@ -3,6 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
4
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
5
  import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
6
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
6
7
  import { resolveScheduleName } from '@/lib/schedule-name'
7
8
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
8
9
  import { notify } from '@/lib/server/ws-hub'
@@ -45,9 +46,13 @@ export async function POST(req: Request) {
45
46
 
46
47
  const candidate = normalizedSchedule.value
47
48
  const agents = loadAgents()
48
- if (!agents[String(candidate.agentId)]) {
49
+ const agent = agents[String(candidate.agentId)]
50
+ if (!agent) {
49
51
  return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
50
52
  }
53
+ if (isAgentDisabled(agent)) {
54
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'take scheduled work') }, { status: 409 })
55
+ }
51
56
  const scheduleType = asScheduleType(candidate.scheduleType)
52
57
  const candidateAgentId = asString(candidate.agentId) || null
53
58
  const candidateTaskPrompt = asString(candidate.taskPrompt)
@@ -19,6 +19,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
19
19
  if (body.service !== undefined) secret.service = body.service
20
20
  if (body.scope !== undefined) secret.scope = body.scope
21
21
  if (body.agentIds !== undefined) secret.agentIds = body.agentIds
22
+ if (body.projectId !== undefined) secret.projectId = body.projectId || undefined
22
23
  secret.updatedAt = Date.now()
23
24
  return secret
24
25
  })
@@ -10,7 +10,7 @@ export async function GET(_req: Request) {
10
10
  const safe = Object.fromEntries(
11
11
  Object.entries(secrets).map(([id, s]: [string, any]) => [
12
12
  id,
13
- { id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, createdAt: s.createdAt, updatedAt: s.updatedAt },
13
+ { id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, projectId: s.projectId, createdAt: s.createdAt, updatedAt: s.updatedAt },
14
14
  ])
15
15
  )
16
16
  return NextResponse.json(safe)
@@ -33,6 +33,7 @@ export async function POST(req: Request) {
33
33
  encryptedValue: encryptKey(body.value),
34
34
  scope: body.scope || 'global',
35
35
  agentIds: body.agentIds || [],
36
+ projectId: typeof body.projectId === 'string' && body.projectId.trim() ? body.projectId.trim() : undefined,
36
37
  createdAt: now,
37
38
  updatedAt: now,
38
39
  }
@@ -126,6 +126,8 @@ export async function PUT(req: Request) {
126
126
  settings.taskQualityGateRequireVerification = parseBoolSetting(settings.taskQualityGateRequireVerification, false)
127
127
  settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
128
128
  settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
129
+ settings.taskManagementEnabled = parseBoolSetting(settings.taskManagementEnabled, true)
130
+ settings.projectManagementEnabled = parseBoolSetting(settings.projectManagementEnabled, true)
129
131
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
130
132
  settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
131
133
  settings.sessionIdleTimeoutSec = parseIntSetting(
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { normalizeOllamaSetupEndpoint } from './route'
5
+
6
+ test('normalizeOllamaSetupEndpoint strips local /v1 suffixes but preserves cloud endpoints', () => {
7
+ assert.equal(
8
+ normalizeOllamaSetupEndpoint('http://localhost:11434/v1', false),
9
+ 'http://localhost:11434',
10
+ )
11
+ assert.equal(
12
+ normalizeOllamaSetupEndpoint('http://localhost:11434/', false),
13
+ 'http://localhost:11434',
14
+ )
15
+ assert.equal(
16
+ normalizeOllamaSetupEndpoint('https://ollama.com/v1', true),
17
+ 'https://ollama.com/v1',
18
+ )
19
+ })
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, decryptKey } from '@/lib/server/storage'
3
3
  import { getDeviceId, wsConnect } from '@/lib/providers/openclaw'
4
4
  import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
5
+ import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
5
6
 
6
7
  type SetupProvider =
7
8
  | 'openai'
@@ -28,6 +29,12 @@ function clean(value: unknown): string {
28
29
  return typeof value === 'string' ? value.trim() : ''
29
30
  }
30
31
 
32
+ export function normalizeOllamaSetupEndpoint(endpoint: string, useCloud: boolean): string {
33
+ const normalized = endpoint.replace(/\/+$/, '')
34
+ if (useCloud) return normalized
35
+ return normalized.replace(/\/v1$/i, '')
36
+ }
37
+
31
38
  function parseBody(input: unknown): SetupCheckBody {
32
39
  if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
33
40
  return input as SetupCheckBody
@@ -101,9 +108,28 @@ async function checkAnthropic(apiKey: string, modelRaw: string): Promise<{ ok: b
101
108
  return { ok: true, message: text ? `Connected to Anthropic. Sample: ${text.slice(0, 120)}` : 'Connected to Anthropic.' }
102
109
  }
103
110
 
104
- async function checkOllama(endpointRaw: string): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; recommendedModel?: string }> {
105
- const normalizedEndpoint = (endpointRaw || 'http://localhost:11434').replace(/\/+$/, '')
106
- const res = await fetch(`${normalizedEndpoint}/api/tags`, {
111
+ async function checkOllama(params: {
112
+ endpointRaw: string
113
+ modelRaw: string
114
+ apiKey?: string
115
+ }): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; recommendedModel?: string }> {
116
+ const runtime = resolveOllamaRuntimeConfig({
117
+ model: params.modelRaw,
118
+ apiKey: params.apiKey,
119
+ apiEndpoint: params.endpointRaw,
120
+ })
121
+ const normalizedEndpoint = normalizeOllamaSetupEndpoint(runtime.endpoint, runtime.useCloud)
122
+ const tagsPath = runtime.useCloud ? '/v1/models' : '/api/tags'
123
+ const headers = runtime.apiKey ? { authorization: `Bearer ${runtime.apiKey}` } : undefined
124
+ if (runtime.useCloud && !runtime.apiKey) {
125
+ return {
126
+ ok: false,
127
+ message: 'Ollama Cloud model requires an API key. Set OLLAMA_API_KEY or attach an Ollama credential.',
128
+ normalizedEndpoint,
129
+ }
130
+ }
131
+ const res = await fetch(`${normalizedEndpoint}${tagsPath}`, {
132
+ headers,
107
133
  signal: AbortSignal.timeout(8_000),
108
134
  cache: 'no-store',
109
135
  })
@@ -112,20 +138,24 @@ async function checkOllama(endpointRaw: string): Promise<{ ok: boolean; message:
112
138
  return { ok: false, message: detail, normalizedEndpoint }
113
139
  }
114
140
  const payload = await res.json().catch(() => ({} as any))
115
- const models = Array.isArray(payload?.models) ? payload.models : []
116
- const firstModel = typeof models[0]?.name === 'string'
117
- ? String(models[0].name).replace(/:latest$/, '')
118
- : undefined
141
+ const models = runtime.useCloud
142
+ ? (Array.isArray(payload?.data) ? payload.data : [])
143
+ : (Array.isArray(payload?.models) ? payload.models : [])
144
+ const firstModel = runtime.useCloud
145
+ ? (typeof models[0]?.id === 'string' ? String(models[0].id) : undefined)
146
+ : (typeof models[0]?.name === 'string' ? String(models[0].name).replace(/:latest$/, '') : undefined)
119
147
  if (models.length === 0) {
120
148
  return {
121
149
  ok: true,
122
- message: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
150
+ message: runtime.useCloud
151
+ ? 'Connected to Ollama Cloud, but no models were returned.'
152
+ : 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
123
153
  normalizedEndpoint,
124
154
  }
125
155
  }
126
156
  return {
127
157
  ok: true,
128
- message: `Connected to Ollama. ${models.length} model(s) available.`,
158
+ message: `Connected to ${runtime.useCloud ? 'Ollama Cloud' : 'Ollama'}. ${models.length} model(s) available.`,
129
159
  normalizedEndpoint,
130
160
  recommendedModel: firstModel,
131
161
  }
@@ -205,7 +235,7 @@ export async function POST(req: Request) {
205
235
  return NextResponse.json(result)
206
236
  }
207
237
  case 'ollama': {
208
- const result = await checkOllama(endpoint)
238
+ const result = await checkOllama({ endpointRaw: endpoint, modelRaw: model, apiKey })
209
239
  return NextResponse.json(result)
210
240
  }
211
241
  case 'openclaw': {
@@ -31,6 +31,18 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
31
31
  content: normalized.content,
32
32
  sourceUrl: normalized.sourceUrl,
33
33
  sourceFormat: normalized.sourceFormat,
34
+ author: normalized.author ?? skill.author,
35
+ tags: normalized.tags ?? skill.tags,
36
+ version: normalized.version ?? null,
37
+ homepage: normalized.homepage ?? null,
38
+ primaryEnv: normalized.primaryEnv ?? null,
39
+ skillKey: normalized.skillKey ?? null,
40
+ always: typeof normalized.always === 'boolean' ? normalized.always : false,
41
+ installOptions: normalized.installOptions,
42
+ skillRequirements: normalized.skillRequirements,
43
+ detectedEnvVars: normalized.detectedEnvVars,
44
+ security: normalized.security,
45
+ frontmatter: normalized.frontmatter,
34
46
  scope: updatedScope,
35
47
  agentIds: updatedAgentIds,
36
48
  id,
@@ -1,6 +1,4 @@
1
- import { genId } from '@/lib/id'
2
1
  import { NextResponse } from 'next/server'
3
- import { loadSkills, saveSkills } from '@/lib/server/storage'
4
2
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
3
 
6
4
  const MAX_SKILL_BYTES = 2 * 1024 * 1024
@@ -46,22 +44,26 @@ export async function POST(req: Request) {
46
44
  sourceUrl: url,
47
45
  })
48
46
 
49
- const skills = loadSkills()
50
- const id = genId()
51
- skills[id] = {
52
- id,
47
+ return NextResponse.json({
53
48
  name: normalized.name,
54
49
  filename: normalized.filename,
55
50
  description: normalized.description,
56
51
  content: normalized.content,
57
52
  sourceUrl: normalized.sourceUrl,
58
53
  sourceFormat: normalized.sourceFormat,
59
- createdAt: Date.now(),
60
- updatedAt: Date.now(),
61
- }
62
- saveSkills(skills)
63
-
64
- return NextResponse.json(skills[id])
54
+ author: normalized.author,
55
+ tags: normalized.tags,
56
+ version: normalized.version,
57
+ homepage: normalized.homepage,
58
+ primaryEnv: normalized.primaryEnv,
59
+ skillKey: normalized.skillKey,
60
+ always: normalized.always,
61
+ installOptions: normalized.installOptions,
62
+ skillRequirements: normalized.skillRequirements,
63
+ detectedEnvVars: normalized.detectedEnvVars,
64
+ security: normalized.security,
65
+ frontmatter: normalized.frontmatter,
66
+ })
65
67
  } catch (err: unknown) {
66
68
  const message = err instanceof Error ? err.message : 'Failed to import skill'
67
69
  return NextResponse.json({ error: message }, { status: 400 })
@@ -5,7 +5,7 @@ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
5
  export const dynamic = 'force-dynamic'
6
6
 
7
7
 
8
- export async function GET(_req: Request) {
8
+ export async function GET() {
9
9
  return NextResponse.json(loadSkills())
10
10
  }
11
11
 
@@ -26,6 +26,18 @@ export async function POST(req: Request) {
26
26
  description: normalized.description || '',
27
27
  sourceUrl: normalized.sourceUrl,
28
28
  sourceFormat: normalized.sourceFormat,
29
+ author: normalized.author,
30
+ tags: normalized.tags,
31
+ version: normalized.version,
32
+ homepage: normalized.homepage,
33
+ primaryEnv: normalized.primaryEnv,
34
+ skillKey: normalized.skillKey,
35
+ always: normalized.always,
36
+ installOptions: normalized.installOptions,
37
+ skillRequirements: normalized.skillRequirements,
38
+ detectedEnvVars: normalized.detectedEnvVars,
39
+ security: normalized.security,
40
+ frontmatter: normalized.frontmatter,
29
41
  scope,
30
42
  agentIds,
31
43
  createdAt: Date.now(),
@@ -126,7 +126,16 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
126
126
  enqueueSystemEvent(tasks[id].sessionId, `Task ${tasks[id].status}: ${tasks[id].title}`)
127
127
  }
128
128
  if (tasks[id].agentId) {
129
- requestHeartbeatNow({ agentId: tasks[id].agentId, reason: 'task-completed' })
129
+ requestHeartbeatNow({
130
+ agentId: tasks[id].agentId,
131
+ eventId: `task:${id}:${tasks[id].status}`,
132
+ reason: 'task-completed',
133
+ source: `task:${id}`,
134
+ resumeMessage: `Task ${tasks[id].status}: ${tasks[id].title}`,
135
+ detail: tasks[id].status === 'failed'
136
+ ? String(tasks[id].error || '').slice(0, 400)
137
+ : JSON.stringify(tasks[id].result || '').slice(0, 400),
138
+ })
130
139
  }
131
140
  }
132
141
 
@@ -0,0 +1,65 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import {
4
+ buildGitHubIssueTaskDescription,
5
+ buildGitHubIssueTaskTags,
6
+ buildGitHubIssueTaskTitle,
7
+ parseGitHubRepoInput,
8
+ } from './route'
9
+
10
+ test('parseGitHubRepoInput accepts repo slugs and GitHub URLs', () => {
11
+ assert.deepEqual(parseGitHubRepoInput('swarmclawai/swarmclaw'), {
12
+ owner: 'swarmclawai',
13
+ repo: 'swarmclaw',
14
+ fullName: 'swarmclawai/swarmclaw',
15
+ })
16
+
17
+ assert.deepEqual(parseGitHubRepoInput('https://github.com/swarmclawai/swarmclaw/issues'), {
18
+ owner: 'swarmclawai',
19
+ repo: 'swarmclaw',
20
+ fullName: 'swarmclawai/swarmclaw',
21
+ })
22
+
23
+ assert.equal(parseGitHubRepoInput('not-a-repo'), null)
24
+ assert.equal(parseGitHubRepoInput('https://example.com/swarmclawai/swarmclaw'), null)
25
+ })
26
+
27
+ test('GitHub issue mapping builds a source-aware task payload shape', () => {
28
+ const issue = {
29
+ id: 12345,
30
+ number: 87,
31
+ title: 'Import GitHub issues into the board',
32
+ body: 'Bring open issues into SwarmClaw tasks.',
33
+ state: 'open',
34
+ html_url: 'https://github.com/swarmclawai/swarmclaw/issues/87',
35
+ labels: [{ name: 'feature' }, { name: 'task board' }, { name: 'feature' }],
36
+ assignee: { login: 'waydelyle' },
37
+ user: { login: 'octocat' },
38
+ }
39
+
40
+ assert.equal(
41
+ buildGitHubIssueTaskTitle(issue, 'swarmclawai/swarmclaw'),
42
+ '[swarmclawai/swarmclaw#87] Import GitHub issues into the board',
43
+ )
44
+
45
+ assert.equal(
46
+ buildGitHubIssueTaskDescription(issue, 'swarmclawai/swarmclaw'),
47
+ [
48
+ 'Imported from GitHub issue swarmclawai/swarmclaw#87',
49
+ 'URL: https://github.com/swarmclawai/swarmclaw/issues/87',
50
+ 'State: open',
51
+ 'Labels: feature, task board, feature',
52
+ 'Assignee: waydelyle',
53
+ 'Opened by: octocat',
54
+ '',
55
+ 'Bring open issues into SwarmClaw tasks.',
56
+ ].join('\n'),
57
+ )
58
+
59
+ assert.deepEqual(buildGitHubIssueTaskTags(issue, 'swarmclawai/swarmclaw'), [
60
+ 'github',
61
+ 'swarmclawai/swarmclaw',
62
+ 'feature',
63
+ 'task board',
64
+ ])
65
+ })