@swarmclawai/swarmclaw 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -0,0 +1,182 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { api } from '@/lib/app/api-client'
3
+ import type {
4
+ ClawHubSkill,
5
+ Skill,
6
+ SkillInvocationConfig,
7
+ SkillCommandDispatch,
8
+ SkillSuggestion,
9
+ } from '@/types'
10
+
11
+ type QueryOptions = {
12
+ enabled?: boolean
13
+ }
14
+
15
+ export interface ClawHubSearchResponse {
16
+ skills: ClawHubSkill[]
17
+ total: number
18
+ page: number
19
+ nextCursor?: string | null
20
+ error?: string
21
+ }
22
+
23
+ export type ClawHubPreview = Partial<Skill> & {
24
+ name: string
25
+ content: string
26
+ description?: string
27
+ invocation?: SkillInvocationConfig | null
28
+ commandDispatch?: SkillCommandDispatch | null
29
+ }
30
+
31
+ async function invalidateSkillQueries(queryClient: ReturnType<typeof useQueryClient>) {
32
+ await queryClient.invalidateQueries({ queryKey: skillQueryKeys.all })
33
+ }
34
+
35
+ export const skillQueryKeys = {
36
+ all: ['skills'] as const,
37
+ }
38
+
39
+ export const skillSuggestionQueryKeys = {
40
+ all: ['skill-suggestions'] as const,
41
+ }
42
+
43
+ export function useSkillsQuery(options: QueryOptions = {}) {
44
+ return useQuery<Record<string, Skill>>({
45
+ queryKey: skillQueryKeys.all,
46
+ queryFn: () => api<Record<string, Skill>>('GET', '/skills'),
47
+ enabled: options.enabled,
48
+ staleTime: 20_000,
49
+ })
50
+ }
51
+
52
+ export function useSkillSuggestionsQuery(options: QueryOptions = {}) {
53
+ return useQuery<SkillSuggestion[]>({
54
+ queryKey: skillSuggestionQueryKeys.all,
55
+ queryFn: () => api<SkillSuggestion[]>('GET', '/skill-suggestions'),
56
+ enabled: options.enabled,
57
+ staleTime: 20_000,
58
+ })
59
+ }
60
+
61
+ export function useImportSkillFromUrlMutation() {
62
+ return useMutation({
63
+ mutationFn: (url: string) =>
64
+ api<Partial<Skill> & { name: string; filename: string; description?: string; content: string; sourceFormat?: 'openclaw' | 'plain' }>(
65
+ 'POST',
66
+ '/skills/import',
67
+ { url },
68
+ ),
69
+ })
70
+ }
71
+
72
+ export function useSaveSkillMutation() {
73
+ const queryClient = useQueryClient()
74
+ return useMutation({
75
+ mutationFn: ({
76
+ id,
77
+ data,
78
+ }: {
79
+ id?: string | null
80
+ data: Record<string, unknown>
81
+ }) => (id ? api('PUT', `/skills/${id}`, data) : api('POST', '/skills', data)),
82
+ onSuccess: async () => {
83
+ await invalidateSkillQueries(queryClient)
84
+ },
85
+ })
86
+ }
87
+
88
+ export function useDeleteSkillMutation() {
89
+ const queryClient = useQueryClient()
90
+ return useMutation({
91
+ mutationFn: (id: string) => api('DELETE', `/skills/${id}`),
92
+ onSuccess: async () => {
93
+ await invalidateSkillQueries(queryClient)
94
+ },
95
+ })
96
+ }
97
+
98
+ export function useGenerateSkillSuggestionMutation() {
99
+ const queryClient = useQueryClient()
100
+ return useMutation({
101
+ mutationFn: (sessionId: string) => api<SkillSuggestion>('POST', '/skill-suggestions', { sessionId }),
102
+ onSuccess: async () => {
103
+ await queryClient.invalidateQueries({ queryKey: skillSuggestionQueryKeys.all })
104
+ },
105
+ })
106
+ }
107
+
108
+ export function useApproveSkillSuggestionMutation() {
109
+ const queryClient = useQueryClient()
110
+ return useMutation({
111
+ mutationFn: (id: string) => api('POST', `/skill-suggestions/${id}/approve`),
112
+ onSuccess: async () => {
113
+ await Promise.all([
114
+ queryClient.invalidateQueries({ queryKey: skillSuggestionQueryKeys.all }),
115
+ invalidateSkillQueries(queryClient),
116
+ ])
117
+ },
118
+ })
119
+ }
120
+
121
+ export function useRejectSkillSuggestionMutation() {
122
+ const queryClient = useQueryClient()
123
+ return useMutation({
124
+ mutationFn: (id: string) => api('POST', `/skill-suggestions/${id}/reject`),
125
+ onSuccess: async () => {
126
+ await queryClient.invalidateQueries({ queryKey: skillSuggestionQueryKeys.all })
127
+ },
128
+ })
129
+ }
130
+
131
+ export function useClawHubSearchMutation() {
132
+ return useMutation({
133
+ mutationFn: ({
134
+ query,
135
+ page,
136
+ limit,
137
+ cursor,
138
+ }: {
139
+ query: string
140
+ page: number
141
+ limit: number
142
+ cursor?: string | null
143
+ }) => {
144
+ const params = new URLSearchParams({
145
+ q: query,
146
+ page: String(page),
147
+ limit: String(limit),
148
+ })
149
+ if (cursor) params.set('cursor', cursor)
150
+ return api<ClawHubSearchResponse>('GET', `/clawhub/search?${params.toString()}`)
151
+ },
152
+ })
153
+ }
154
+
155
+ export function useClawHubPreviewMutation() {
156
+ return useMutation({
157
+ mutationFn: (payload: {
158
+ name: string
159
+ description?: string
160
+ author?: string
161
+ tags?: string[]
162
+ url: string
163
+ }) => api<ClawHubPreview>('POST', '/clawhub/preview', payload),
164
+ })
165
+ }
166
+
167
+ export function useInstallClawHubSkillMutation() {
168
+ const queryClient = useQueryClient()
169
+ return useMutation({
170
+ mutationFn: (payload: {
171
+ name: string
172
+ description?: string
173
+ url: string
174
+ author?: string
175
+ tags?: string[]
176
+ content?: string
177
+ }) => api('POST', '/clawhub/install', payload),
178
+ onSuccess: async () => {
179
+ await invalidateSkillQueries(queryClient)
180
+ },
181
+ })
182
+ }
@@ -0,0 +1,189 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { api } from '@/lib/app/api-client'
3
+ import {
4
+ bulkUpdateTasks,
5
+ createTask,
6
+ fetchTasks,
7
+ importGitHubIssues,
8
+ updateTask,
9
+ type GitHubIssueImportRequest,
10
+ type GitHubIssueImportResult,
11
+ } from '@/lib/tasks'
12
+ import type { BoardTask, BoardTaskStatus, TaskComment } from '@/types'
13
+
14
+ export type {
15
+ GitHubIssueImportRequest,
16
+ GitHubIssueImportResult,
17
+ } from '@/lib/tasks'
18
+
19
+ type TasksRecord = Record<string, BoardTask>
20
+ type QueryOptions = {
21
+ enabled?: boolean
22
+ includeArchived?: boolean
23
+ }
24
+ type TasksSnapshot = Array<[readonly unknown[], TasksRecord | undefined]>
25
+
26
+ export const taskQueryKeys = {
27
+ all: ['tasks'] as const,
28
+ lists: () => ['tasks', 'list'] as const,
29
+ list: (params: { includeArchived: boolean }) => ['tasks', 'list', params] as const,
30
+ }
31
+
32
+ function includeArchivedFromKey(key: readonly unknown[]): boolean {
33
+ const params = key[2]
34
+ return typeof params === 'object' && params !== null && (params as { includeArchived?: boolean }).includeArchived === true
35
+ }
36
+
37
+ function captureTaskSnapshots(queryClient: ReturnType<typeof useQueryClient>): TasksSnapshot {
38
+ return queryClient.getQueriesData<TasksRecord>({ queryKey: taskQueryKeys.lists() }) as TasksSnapshot
39
+ }
40
+
41
+ function restoreTaskSnapshots(
42
+ queryClient: ReturnType<typeof useQueryClient>,
43
+ snapshots: TasksSnapshot | undefined,
44
+ ): void {
45
+ if (!snapshots) return
46
+ for (const [key, data] of snapshots) {
47
+ queryClient.setQueryData(key, data)
48
+ }
49
+ }
50
+
51
+ function applyTaskListPatch(
52
+ current: TasksRecord | undefined,
53
+ taskId: string,
54
+ nextTask: BoardTask | null,
55
+ includeArchived: boolean,
56
+ ): TasksRecord | undefined {
57
+ if (!current) return current
58
+ const next = { ...current }
59
+ if (!nextTask || (!includeArchived && nextTask.status === 'archived')) {
60
+ delete next[taskId]
61
+ return next
62
+ }
63
+ next[taskId] = nextTask
64
+ return next
65
+ }
66
+
67
+ function patchTaskCaches(
68
+ queryClient: ReturnType<typeof useQueryClient>,
69
+ updater: (current: TasksRecord | undefined, includeArchived: boolean) => TasksRecord | undefined,
70
+ ): TasksSnapshot {
71
+ const snapshots = captureTaskSnapshots(queryClient)
72
+ for (const [key] of snapshots) {
73
+ queryClient.setQueryData<TasksRecord>(key, (current) => updater(current, includeArchivedFromKey(key)))
74
+ }
75
+ return snapshots
76
+ }
77
+
78
+ export function useTasksQuery(options: QueryOptions = {}) {
79
+ const includeArchived = options.includeArchived ?? false
80
+ return useQuery<TasksRecord>({
81
+ queryKey: taskQueryKeys.list({ includeArchived }),
82
+ queryFn: () => fetchTasks(includeArchived),
83
+ enabled: options.enabled,
84
+ staleTime: 10_000,
85
+ })
86
+ }
87
+
88
+ export function useCreateTaskMutation() {
89
+ const queryClient = useQueryClient()
90
+ return useMutation({
91
+ mutationFn: createTask,
92
+ onSuccess: async () => {
93
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
94
+ },
95
+ })
96
+ }
97
+
98
+ export function useUpdateTaskMutation() {
99
+ const queryClient = useQueryClient()
100
+ return useMutation({
101
+ mutationFn: ({ id, patch }: { id: string; patch: Partial<BoardTask> }) => updateTask(id, patch),
102
+ onMutate: async ({ id, patch }) => {
103
+ await queryClient.cancelQueries({ queryKey: taskQueryKeys.lists() })
104
+ const snapshots = patchTaskCaches(queryClient, (current, includeArchived) => {
105
+ const existing = current?.[id]
106
+ if (!existing) return current
107
+ const nextTask = { ...existing, ...patch, updatedAt: Date.now() }
108
+ return applyTaskListPatch(current, id, nextTask, includeArchived)
109
+ })
110
+ return { snapshots }
111
+ },
112
+ onError: (_error, _variables, context) => {
113
+ restoreTaskSnapshots(queryClient, context?.snapshots)
114
+ },
115
+ onSettled: async () => {
116
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
117
+ },
118
+ })
119
+ }
120
+
121
+ export function useBulkUpdateTasksMutation() {
122
+ const queryClient = useQueryClient()
123
+ return useMutation({
124
+ mutationFn: ({ ids, patch }: { ids: string[]; patch: { status?: BoardTaskStatus; agentId?: string; projectId?: string | null } }) =>
125
+ bulkUpdateTasks(ids, patch),
126
+ onMutate: async ({ ids, patch }) => {
127
+ await queryClient.cancelQueries({ queryKey: taskQueryKeys.lists() })
128
+ const snapshots = patchTaskCaches(queryClient, (current, includeArchived) => {
129
+ if (!current) return current
130
+ const next = { ...current }
131
+ for (const id of ids) {
132
+ const existing = next[id]
133
+ if (!existing) continue
134
+ const updated: BoardTask = {
135
+ ...existing,
136
+ ...patch,
137
+ agentId: patch.agentId ?? existing.agentId,
138
+ projectId: patch.projectId === undefined ? existing.projectId : patch.projectId ?? undefined,
139
+ updatedAt: Date.now(),
140
+ }
141
+ if (!includeArchived && updated.status === 'archived') {
142
+ delete next[id]
143
+ continue
144
+ }
145
+ next[id] = updated
146
+ }
147
+ return next
148
+ })
149
+ return { snapshots }
150
+ },
151
+ onError: (_error, _variables, context) => {
152
+ restoreTaskSnapshots(queryClient, context?.snapshots)
153
+ },
154
+ onSettled: async () => {
155
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
156
+ },
157
+ })
158
+ }
159
+
160
+ export function useClearDoneTasksMutation() {
161
+ const queryClient = useQueryClient()
162
+ return useMutation({
163
+ mutationFn: () => api('DELETE', '/tasks?filter=done'),
164
+ onSuccess: async () => {
165
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
166
+ },
167
+ })
168
+ }
169
+
170
+ export function useImportGitHubIssuesMutation() {
171
+ const queryClient = useQueryClient()
172
+ return useMutation<GitHubIssueImportResult, Error, GitHubIssueImportRequest>({
173
+ mutationFn: importGitHubIssues,
174
+ onSuccess: async () => {
175
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
176
+ },
177
+ })
178
+ }
179
+
180
+ export function useAppendTaskCommentMutation() {
181
+ const queryClient = useQueryClient()
182
+ return useMutation({
183
+ mutationFn: ({ id, comment }: { id: string; comment: TaskComment }) =>
184
+ updateTask(id, { appendComment: comment } as Partial<BoardTask> & { appendComment: TaskComment }),
185
+ onSettled: async () => {
186
+ await queryClient.invalidateQueries({ queryKey: taskQueryKeys.all })
187
+ },
188
+ })
189
+ }
@@ -2,14 +2,15 @@
2
2
 
3
3
  import { useEffect, useRef } from 'react'
4
4
  import { subscribeWs, unsubscribeWs, isWsConnected, onWsStateChange, offWsStateChange } from '@/lib/ws-client'
5
+ import { hmrSingleton } from '@/lib/shared-utils'
5
6
  import { usePageActive } from './use-page-active'
6
7
 
7
8
  /** Shared fallback intervals keyed by topic — multiple useWs instances share one interval. */
8
- const sharedFallbacks = new Map<string, {
9
+ const sharedFallbacks = hmrSingleton('useWs_sharedFallbacks', () => new Map<string, {
9
10
  interval: ReturnType<typeof setInterval> | null
10
11
  handlers: Set<() => void>
11
12
  ms: number
12
- }>()
13
+ }>())
13
14
 
14
15
  function runAllHandlers(topic: string): void {
15
16
  const entry = sharedFallbacks.get(topic)
@@ -1,12 +1,12 @@
1
1
  import { fetchWithTimeout, isAbortError, isTimeoutError } from '@/lib/fetch-timeout'
2
2
  import { safeStorageGet, safeStorageSet, safeStorageRemove } from './safe-storage'
3
- import { sleep } from '@/lib/shared-utils'
3
+ import { sleep, hmrSingleton } from '@/lib/shared-utils'
4
4
 
5
5
  const ACCESS_KEY_STORAGE = 'sc_access_key'
6
6
  const DEFAULT_API_TIMEOUT_MS = 12_000
7
7
  const DEFAULT_GET_RETRIES = 2
8
8
  const RETRY_DELAY_BASE_MS = 300
9
- const inflightGetRequests = new Map<string, Promise<unknown>>()
9
+ const inflightGetRequests = hmrSingleton('apiClient_inflightGetRequests', () => new Map<string, Promise<unknown>>())
10
10
 
11
11
  export function getStoredAccessKey(): string {
12
12
  return safeStorageGet(ACCESS_KEY_STORAGE) || ''
@@ -76,3 +76,111 @@ test('builtin provider override records do not surface as custom providers', ()
76
76
 
77
77
  assert.equal(output.openAiCount, 1)
78
78
  })
79
+
80
+ test('custom provider resolution includes defaultEndpoint and optionalApiKey', () => {
81
+ const output = runWithTempDataDir<{
82
+ defaultEndpoint: string | null
83
+ optionalApiKey: boolean | null
84
+ requiresApiKey: boolean | null
85
+ }>(`
86
+ const storageModule = await import('@/lib/server/storage')
87
+ const storage = storageModule.default || storageModule
88
+ storage.saveProviderConfigs({
89
+ 'custom-llama-server': {
90
+ id: 'custom-llama-server',
91
+ name: 'llama-server',
92
+ type: 'custom',
93
+ baseUrl: 'http://127.0.0.1:8080/v1',
94
+ models: ['my-model'],
95
+ requiresApiKey: false,
96
+ credentialId: null,
97
+ isEnabled: true,
98
+ createdAt: 1,
99
+ updatedAt: 1,
100
+ },
101
+ })
102
+
103
+ const providersModule = await import('@/lib/providers/index')
104
+ const providers = providersModule.default || providersModule
105
+ const resolved = providers.getProvider('custom-llama-server')
106
+
107
+ console.log(JSON.stringify({
108
+ defaultEndpoint: resolved?.defaultEndpoint ?? null,
109
+ optionalApiKey: resolved?.optionalApiKey ?? null,
110
+ requiresApiKey: resolved?.requiresApiKey ?? null,
111
+ }))
112
+ `)
113
+
114
+ assert.equal(output.defaultEndpoint, 'http://127.0.0.1:8080/v1')
115
+ assert.equal(output.optionalApiKey, true)
116
+ assert.equal(output.requiresApiKey, false)
117
+ })
118
+
119
+ test('custom provider with uuid-style ID resolves correctly', () => {
120
+ const output = runWithTempDataDir<{
121
+ resolvedName: string | null
122
+ hasHandler: boolean
123
+ }>(`
124
+ const storageModule = await import('@/lib/server/storage')
125
+ const storage = storageModule.default || storageModule
126
+ storage.saveProviderConfigs({
127
+ 'custom-d20b934e': {
128
+ id: 'custom-d20b934e',
129
+ name: 'My llama-server',
130
+ type: 'custom',
131
+ baseUrl: 'http://127.0.0.1:8080/v1',
132
+ models: ['llama-3.1-8b'],
133
+ requiresApiKey: false,
134
+ credentialId: null,
135
+ isEnabled: true,
136
+ createdAt: 1,
137
+ updatedAt: 1,
138
+ },
139
+ })
140
+
141
+ const providersModule = await import('@/lib/providers/index')
142
+ const providers = providersModule.default || providersModule
143
+ const resolved = providers.getProvider('custom-d20b934e')
144
+
145
+ console.log(JSON.stringify({
146
+ resolvedName: resolved?.name ?? null,
147
+ hasHandler: typeof resolved?.handler?.streamChat === 'function',
148
+ }))
149
+ `)
150
+
151
+ assert.equal(output.resolvedName, 'My llama-server')
152
+ assert.equal(output.hasHandler, true)
153
+ })
154
+
155
+ test('disabled custom providers are not resolved by getProvider', () => {
156
+ const output = runWithTempDataDir<{
157
+ resolved: boolean
158
+ }>(`
159
+ const storageModule = await import('@/lib/server/storage')
160
+ const storage = storageModule.default || storageModule
161
+ storage.saveProviderConfigs({
162
+ 'custom-disabled': {
163
+ id: 'custom-disabled',
164
+ name: 'Disabled Provider',
165
+ type: 'custom',
166
+ baseUrl: 'http://127.0.0.1:8080/v1',
167
+ models: ['test'],
168
+ requiresApiKey: false,
169
+ credentialId: null,
170
+ isEnabled: false,
171
+ createdAt: 1,
172
+ updatedAt: 1,
173
+ },
174
+ })
175
+
176
+ const providersModule = await import('@/lib/providers/index')
177
+ const providers = providersModule.default || providersModule
178
+ const resolved = providers.getProvider('custom-disabled')
179
+
180
+ console.log(JSON.stringify({
181
+ resolved: resolved !== null,
182
+ }))
183
+ `)
184
+
185
+ assert.equal(output.resolved, false)
186
+ })
@@ -286,7 +286,8 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
286
286
  return Object.fromEntries(
287
287
  Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
288
288
  )
289
- } catch {
289
+ } catch (err) {
290
+ log.warn(TAG, 'Failed to load custom providers from storage', errorMessage(err))
290
291
  return {}
291
292
  }
292
293
  }
@@ -325,6 +326,7 @@ export function getProviderList(): ProviderInfo[] {
325
326
  defaultModels: c.models,
326
327
  supportsModelDiscovery: false,
327
328
  requiresApiKey: c.requiresApiKey,
329
+ optionalApiKey: !c.requiresApiKey,
328
330
  requiresEndpoint: false as boolean,
329
331
  defaultEndpoint: c.baseUrl,
330
332
  }))
@@ -348,26 +350,47 @@ export function getProviderList(): ProviderInfo[] {
348
350
  return [...builtins, ...customs, ...extensionProviders]
349
351
  }
350
352
 
353
+ function buildCustomProviderConfig(custom: CustomProviderConfig): BuiltinProviderConfig {
354
+ return {
355
+ id: custom.id as ProviderId,
356
+ name: custom.name,
357
+ models: custom.models,
358
+ requiresApiKey: custom.requiresApiKey,
359
+ optionalApiKey: !custom.requiresApiKey,
360
+ requiresEndpoint: false,
361
+ defaultEndpoint: custom.baseUrl,
362
+ handler: {
363
+ streamChat: async (opts) => {
364
+ const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
365
+ const { streamOpenAiChat } = await import('./openai')
366
+ return streamOpenAiChat({ ...opts, session: patchedSession })
367
+ },
368
+ },
369
+ }
370
+ }
371
+
351
372
  export function getProvider(id: string): BuiltinProviderConfig | null {
352
373
  if (PROVIDERS[id]) return PROVIDERS[id]
353
-
374
+
354
375
  // Check custom providers
355
376
  const customs = getCustomProviders()
356
377
  const custom = customs[id]
357
378
  if (custom?.isEnabled) {
358
- return {
359
- id: custom.id as ProviderId,
360
- name: custom.name,
361
- models: custom.models,
362
- requiresApiKey: custom.requiresApiKey,
363
- requiresEndpoint: false,
364
- handler: {
365
- streamChat: async (opts) => {
366
- const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
367
- const { streamOpenAiChat } = await import('./openai')
368
- return streamOpenAiChat({ ...opts, session: patchedSession })
369
- },
370
- },
379
+ return buildCustomProviderConfig(custom)
380
+ }
381
+
382
+ // Fallback: direct single-item DB lookup for custom-* providers
383
+ if (id.startsWith('custom-') && !custom) {
384
+ try {
385
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
386
+ const { loadStoredItem } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
387
+ const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
388
+ if (directConfig?.type === 'custom' && directConfig.isEnabled) {
389
+ log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
390
+ return buildCustomProviderConfig(directConfig)
391
+ }
392
+ } catch (err) {
393
+ log.warn(TAG, `Direct DB lookup failed for provider '${id}'`, errorMessage(err))
371
394
  }
372
395
  }
373
396
 
@@ -0,0 +1,17 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+
3
+ export function createAppQueryClient(): QueryClient {
4
+ return new QueryClient({
5
+ defaultOptions: {
6
+ queries: {
7
+ staleTime: 15_000,
8
+ gcTime: 5 * 60_000,
9
+ retry: 0,
10
+ refetchOnWindowFocus: false,
11
+ },
12
+ mutations: {
13
+ retry: 0,
14
+ },
15
+ },
16
+ })
17
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  } from '@/types'
8
8
  import { deriveOpenClawWsUrl, normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
9
9
  import { resolveProviderApiEndpoint, resolveProviderCredentialId } from '@/lib/server/provider-endpoint'
10
- import { loadGatewayProfiles } from '@/lib/server/storage'
10
+ import { loadGatewayProfiles } from '@/lib/server/gateways/gateway-profile-repository'
11
11
  import { isProviderCoolingDown } from '@/lib/server/provider-health'
12
12
 
13
13
  const DEFAULT_OPENCLAW_ENDPOINT = 'http://localhost:18789/v1'