@swarmclawai/swarmclaw 0.7.8 → 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 (251) hide show
  1. package/README.md +12 -15
  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 +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -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
+ })
@@ -0,0 +1,337 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { genId } from '@/lib/id'
4
+ import { computeTaskFingerprint } from '@/lib/task-dedupe'
5
+ import { formatZodError } from '@/lib/validation/schemas'
6
+ import { loadSettings, loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
7
+ import { notify } from '@/lib/server/ws-hub'
8
+ import type { BoardTask } from '@/types'
9
+
10
+ const MAX_IMPORT_LIMIT = 200
11
+ const BODY_CHAR_LIMIT = 12_000
12
+
13
+ const GitHubIssueImportSchema = z.object({
14
+ repo: z.string().trim().min(1, 'Repository is required'),
15
+ token: z.string().trim().optional().default(''),
16
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
17
+ limit: z.coerce.number().int().min(1).max(MAX_IMPORT_LIMIT).optional().default(25),
18
+ labels: z.array(z.string()).optional().default([]),
19
+ projectId: z.string().trim().nullable().optional().default(null),
20
+ agentId: z.string().trim().nullable().optional().default(null),
21
+ })
22
+
23
+ type GitHubIssueLabel = string | { name?: string | null }
24
+
25
+ interface GitHubIssueRecord {
26
+ id: number | string
27
+ number: number
28
+ title: string
29
+ body?: string | null
30
+ state?: string | null
31
+ html_url?: string | null
32
+ labels?: GitHubIssueLabel[]
33
+ assignee?: { login?: string | null } | null
34
+ user?: { login?: string | null } | null
35
+ pull_request?: unknown
36
+ }
37
+
38
+ interface ParsedRepo {
39
+ owner: string
40
+ repo: string
41
+ fullName: string
42
+ }
43
+
44
+ function getGitHubToken(explicitToken: string): string {
45
+ return explicitToken.trim()
46
+ || process.env.GITHUB_TOKEN
47
+ || process.env.GH_TOKEN
48
+ || process.env.GITHUB_PERSONAL_ACCESS_TOKEN
49
+ || ''
50
+ }
51
+
52
+ function normalizeLabelName(label: GitHubIssueLabel): string {
53
+ if (typeof label === 'string') return label.trim()
54
+ return String(label?.name || '').trim()
55
+ }
56
+
57
+ function normalizeTag(value: string): string {
58
+ return value.trim().replace(/\s+/g, ' ').slice(0, 60)
59
+ }
60
+
61
+ function toIssueSummary(issue: GitHubIssueRecord, taskId?: string) {
62
+ return {
63
+ taskId,
64
+ number: issue.number,
65
+ title: issue.title || `Issue ${issue.number}`,
66
+ url: issue.html_url || null,
67
+ }
68
+ }
69
+
70
+ export function parseGitHubRepoInput(input: string): ParsedRepo | null {
71
+ const trimmed = input.trim().replace(/\.git$/i, '')
72
+ if (!trimmed) return null
73
+
74
+ if (/^https?:\/\//i.test(trimmed)) {
75
+ try {
76
+ const url = new URL(trimmed)
77
+ if (!/github\.com$/i.test(url.hostname)) return null
78
+ const parts = url.pathname.split('/').filter(Boolean)
79
+ if (parts.length < 2) return null
80
+ const owner = parts[0]
81
+ const repo = parts[1].replace(/\.git$/i, '')
82
+ if (!owner || !repo) return null
83
+ return { owner, repo, fullName: `${owner}/${repo}` }
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ const compact = trimmed.replace(/^github\.com\//i, '')
90
+ const parts = compact.split('/').filter(Boolean)
91
+ if (parts.length < 2) return null
92
+ const owner = parts[0]
93
+ const repo = parts[1].replace(/\.git$/i, '')
94
+ if (!owner || !repo) return null
95
+ return { owner, repo, fullName: `${owner}/${repo}` }
96
+ }
97
+
98
+ export function buildGitHubIssueTaskTitle(issue: GitHubIssueRecord, repoFullName: string): string {
99
+ const title = issue.title?.trim() || `Issue ${issue.number}`
100
+ return `[${repoFullName}#${issue.number}] ${title}`
101
+ }
102
+
103
+ export function buildGitHubIssueTaskDescription(issue: GitHubIssueRecord, repoFullName: string): string {
104
+ const labels = (issue.labels || [])
105
+ .map(normalizeLabelName)
106
+ .filter(Boolean)
107
+ const header = [
108
+ `Imported from GitHub issue ${repoFullName}#${issue.number}`,
109
+ issue.html_url ? `URL: ${issue.html_url}` : '',
110
+ issue.state ? `State: ${issue.state}` : '',
111
+ labels.length > 0 ? `Labels: ${labels.join(', ')}` : '',
112
+ issue.assignee?.login ? `Assignee: ${issue.assignee.login}` : '',
113
+ issue.user?.login ? `Opened by: ${issue.user.login}` : '',
114
+ ]
115
+ .filter(Boolean)
116
+ .join('\n')
117
+
118
+ const rawBody = String(issue.body || '').trim()
119
+ if (!rawBody) return header
120
+
121
+ const body = rawBody.length > BODY_CHAR_LIMIT
122
+ ? `${rawBody.slice(0, BODY_CHAR_LIMIT).trimEnd()}\n\n[Truncated during import]`
123
+ : rawBody
124
+
125
+ return `${header}\n\n${body}`
126
+ }
127
+
128
+ export function buildGitHubIssueTaskTags(issue: GitHubIssueRecord, repoFullName: string): string[] {
129
+ const raw = [
130
+ 'github',
131
+ repoFullName,
132
+ ...(issue.labels || []).map(normalizeLabelName),
133
+ ]
134
+ return Array.from(new Set(raw.map(normalizeTag).filter(Boolean))).slice(0, 8)
135
+ }
136
+
137
+ function findExistingImportedTask(
138
+ tasks: Record<string, BoardTask>,
139
+ repoFullName: string,
140
+ issueNumber: number,
141
+ ): BoardTask | null {
142
+ for (const task of Object.values(tasks)) {
143
+ if (task.sourceType !== 'import') continue
144
+ if (task.externalSource?.source !== 'github') continue
145
+ if (task.externalSource?.repo !== repoFullName) continue
146
+ if (task.externalSource?.number !== issueNumber) continue
147
+ return task
148
+ }
149
+ return null
150
+ }
151
+
152
+ async function fetchGitHubIssues(args: {
153
+ owner: string
154
+ repo: string
155
+ state: 'open' | 'closed' | 'all'
156
+ limit: number
157
+ labels: string[]
158
+ token: string
159
+ }): Promise<GitHubIssueRecord[]> {
160
+ const headers: Record<string, string> = {
161
+ Accept: 'application/vnd.github+json',
162
+ 'User-Agent': 'SwarmClaw',
163
+ 'X-GitHub-Api-Version': '2022-11-28',
164
+ }
165
+ if (args.token) headers.Authorization = `Bearer ${args.token}`
166
+
167
+ const results: GitHubIssueRecord[] = []
168
+ const perPage = Math.min(100, Math.max(30, args.limit))
169
+ const maxPages = Math.max(1, Math.ceil(args.limit / 100) + 2)
170
+
171
+ for (let page = 1; page <= maxPages && results.length < args.limit; page++) {
172
+ const url = new URL(`https://api.github.com/repos/${args.owner}/${args.repo}/issues`)
173
+ url.searchParams.set('state', args.state)
174
+ url.searchParams.set('per_page', String(perPage))
175
+ url.searchParams.set('page', String(page))
176
+ if (args.labels.length > 0) url.searchParams.set('labels', args.labels.join(','))
177
+
178
+ const response = await fetch(url, {
179
+ headers,
180
+ cache: 'no-store',
181
+ })
182
+
183
+ if (!response.ok) {
184
+ const payload = await response.json().catch(() => null) as { message?: unknown } | null
185
+ const message = typeof payload?.message === 'string'
186
+ ? payload.message
187
+ : `GitHub request failed (${response.status})`
188
+ const err = new Error(message) as Error & { status?: number }
189
+ err.status = response.status
190
+ throw err
191
+ }
192
+
193
+ const payload = await response.json().catch(() => null) as unknown
194
+ if (!Array.isArray(payload)) {
195
+ throw new Error('GitHub returned an unexpected response.')
196
+ }
197
+
198
+ const pageIssues = payload
199
+ .filter((entry): entry is GitHubIssueRecord => !!entry && typeof entry === 'object')
200
+ .filter((entry) => !entry.pull_request)
201
+
202
+ results.push(...pageIssues)
203
+ if (payload.length < perPage) break
204
+ }
205
+
206
+ return results.slice(0, args.limit)
207
+ }
208
+
209
+ export async function POST(req: Request) {
210
+ const raw = await req.json().catch(() => null)
211
+ const parsed = GitHubIssueImportSchema.safeParse(raw)
212
+ if (!parsed.success) {
213
+ return NextResponse.json(formatZodError(parsed.error), { status: 400 })
214
+ }
215
+
216
+ const repo = parseGitHubRepoInput(parsed.data.repo)
217
+ if (!repo) {
218
+ return NextResponse.json({ error: 'Use a GitHub repo like owner/repo or a github.com URL.' }, { status: 400 })
219
+ }
220
+
221
+ const labels = parsed.data.labels
222
+ .map((value) => String(value || '').trim())
223
+ .filter(Boolean)
224
+
225
+ let issues: GitHubIssueRecord[]
226
+ try {
227
+ issues = await fetchGitHubIssues({
228
+ owner: repo.owner,
229
+ repo: repo.repo,
230
+ state: parsed.data.state,
231
+ limit: parsed.data.limit,
232
+ labels,
233
+ token: getGitHubToken(parsed.data.token),
234
+ })
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : 'GitHub import failed.'
237
+ const status = typeof (err as { status?: unknown })?.status === 'number'
238
+ ? Number((err as { status?: number }).status)
239
+ : 500
240
+ const responseStatus = [400, 401, 403, 404, 429].includes(status) ? status : 502
241
+ return NextResponse.json({ error: message }, { status: responseStatus })
242
+ }
243
+
244
+ const tasks = loadTasks() as Record<string, BoardTask>
245
+ const settings = loadSettings()
246
+ const now = Date.now()
247
+ const maxAttempts = Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
248
+ const retryBackoffSec = Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
249
+ const projectId = parsed.data.projectId || undefined
250
+ const agentId = parsed.data.agentId || ''
251
+
252
+ const created: Array<ReturnType<typeof toIssueSummary>> = []
253
+ const skipped: Array<ReturnType<typeof toIssueSummary>> = []
254
+ const taskEntries: Array<[string, BoardTask]> = []
255
+
256
+ for (const issue of issues) {
257
+ const existing = findExistingImportedTask(tasks, repo.fullName, issue.number)
258
+ if (existing) {
259
+ skipped.push(toIssueSummary(issue, existing.id))
260
+ continue
261
+ }
262
+
263
+ const id = genId()
264
+ const title = buildGitHubIssueTaskTitle(issue, repo.fullName)
265
+ const task: BoardTask = {
266
+ id,
267
+ title,
268
+ description: buildGitHubIssueTaskDescription(issue, repo.fullName),
269
+ status: 'backlog',
270
+ agentId,
271
+ projectId,
272
+ result: null,
273
+ error: null,
274
+ outputFiles: [],
275
+ artifacts: [],
276
+ createdAt: now,
277
+ updatedAt: now,
278
+ queuedAt: null,
279
+ startedAt: null,
280
+ completedAt: null,
281
+ archivedAt: null,
282
+ attempts: 0,
283
+ maxAttempts,
284
+ retryBackoffSec,
285
+ retryScheduledAt: null,
286
+ deadLetteredAt: null,
287
+ checkpoint: null,
288
+ blockedBy: [],
289
+ blocks: [],
290
+ tags: buildGitHubIssueTaskTags(issue, repo.fullName),
291
+ sourceType: 'import',
292
+ externalSource: {
293
+ source: 'github',
294
+ id: String(issue.id),
295
+ repo: repo.fullName,
296
+ number: issue.number,
297
+ state: issue.state || null,
298
+ labels: (issue.labels || []).map(normalizeLabelName).filter(Boolean),
299
+ assignee: issue.assignee?.login || null,
300
+ url: issue.html_url || null,
301
+ },
302
+ fingerprint: computeTaskFingerprint(title, agentId),
303
+ }
304
+
305
+ tasks[id] = task
306
+ taskEntries.push([id, task])
307
+ created.push(toIssueSummary(issue, id))
308
+ }
309
+
310
+ if (taskEntries.length > 0) {
311
+ upsertStoredItems('tasks', taskEntries)
312
+ notify('tasks')
313
+ }
314
+
315
+ logActivity({
316
+ entityType: 'task',
317
+ entityId: created[0]?.taskId || `github:${repo.fullName}`,
318
+ action: 'imported',
319
+ actor: 'user',
320
+ summary: `GitHub import from ${repo.fullName}: ${created.length} created, ${skipped.length} skipped`,
321
+ detail: {
322
+ repo: repo.fullName,
323
+ state: parsed.data.state,
324
+ labels,
325
+ created: created.length,
326
+ skipped: skipped.length,
327
+ },
328
+ })
329
+
330
+ return NextResponse.json({
331
+ repo: repo.fullName,
332
+ state: parsed.data.state,
333
+ fetched: issues.length,
334
+ created,
335
+ skipped,
336
+ })
337
+ }
@@ -1,8 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
3
- import { sendSol } from '@/lib/server/solana'
4
3
  import { notify } from '@/lib/server/ws-hub'
5
4
  import type { AgentWallet, WalletTransaction } from '@/types'
5
+ import { getWalletAtomicAmount } from '@/lib/wallet'
6
+ import { sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
6
7
  export const dynamic = 'force-dynamic'
7
8
 
8
9
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -41,10 +42,23 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
41
42
 
42
43
  // Approve — sign and submit
43
44
  try {
44
- const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
45
+ const limitError = validateWalletSendLimits({ wallet, amountAtomic: getWalletAtomicAmount(tx), excludeTransactionId: transactionId })
46
+ if (limitError) {
47
+ tx.status = 'failed'
48
+ upsertWalletTransaction(transactionId, tx)
49
+ notify('wallets')
50
+ return NextResponse.json({
51
+ error: limitError,
52
+ transactionId,
53
+ status: 'failed',
54
+ }, { status: limitError === 'Amount must be positive' ? 400 : 403 })
55
+ }
56
+
57
+ const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, tx.toAddress, getWalletAtomicAmount(tx))
45
58
  tx.status = 'confirmed'
46
59
  tx.signature = signature
47
- tx.feeLamports = fee
60
+ tx.feeAtomic = feeAtomic
61
+ tx.feeLamports = wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined
48
62
  tx.approvedBy = 'user'
49
63
  upsertWalletTransaction(transactionId, tx)
50
64
  notify('wallets')