@swarmclawai/swarmclaw 1.2.3 → 1.2.5

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 (273) hide show
  1. package/README.md +20 -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]/models/route.test.ts +60 -0
  49. package/src/app/api/providers/[id]/models/route.ts +33 -1
  50. package/src/app/api/providers/[id]/route.test.ts +49 -0
  51. package/src/app/api/providers/[id]/route.ts +30 -1
  52. package/src/app/api/providers/ollama/route.ts +6 -5
  53. package/src/app/api/schedules/[id]/route.ts +14 -108
  54. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  55. package/src/app/api/schedules/route.ts +9 -51
  56. package/src/app/api/settings/route.ts +4 -3
  57. package/src/app/api/setup/check-provider/route.ts +15 -1
  58. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  59. package/src/app/api/system/status/route.ts +2 -2
  60. package/src/app/api/tasks/[id]/route.ts +16 -202
  61. package/src/app/api/tasks/bulk/route.ts +5 -86
  62. package/src/app/api/tasks/metrics/route.ts +2 -1
  63. package/src/app/api/tasks/route.ts +11 -171
  64. package/src/app/api/upload/route.ts +1 -1
  65. package/src/app/api/uploads/[filename]/route.ts +1 -1
  66. package/src/app/api/uploads/route.ts +1 -1
  67. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  68. package/src/app/layout.tsx +9 -6
  69. package/src/app/protocols/page.tsx +71 -89
  70. package/src/app/tasks/page.tsx +32 -32
  71. package/src/cli/index.js +1 -0
  72. package/src/cli/spec.js +1 -0
  73. package/src/components/agents/agent-sheet.tsx +51 -25
  74. package/src/components/agents/inspector-panel.tsx +15 -4
  75. package/src/components/auth/setup-wizard/index.tsx +27 -18
  76. package/src/components/auth/setup-wizard/shared.tsx +2 -2
  77. package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
  78. package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
  79. package/src/components/auth/setup-wizard/types.ts +6 -4
  80. package/src/components/auth/setup-wizard/utils.test.ts +38 -8
  81. package/src/components/auth/setup-wizard/utils.ts +14 -8
  82. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  83. package/src/components/connectors/connector-list.tsx +26 -40
  84. package/src/components/connectors/connector-sheet.tsx +95 -149
  85. package/src/components/gateways/gateway-sheet.tsx +61 -110
  86. package/src/components/layout/live-query-sync.tsx +121 -0
  87. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  88. package/src/components/providers/app-query-provider.tsx +17 -0
  89. package/src/components/providers/provider-list.tsx +150 -77
  90. package/src/components/providers/provider-sheet.tsx +102 -77
  91. package/src/components/shared/model-combobox.tsx +5 -4
  92. package/src/components/skills/skill-list.tsx +5 -18
  93. package/src/components/skills/skill-sheet.tsx +21 -20
  94. package/src/components/skills/skills-workspace.tsx +48 -87
  95. package/src/components/tasks/task-card.tsx +20 -13
  96. package/src/components/tasks/task-column.tsx +22 -7
  97. package/src/components/tasks/task-list.tsx +8 -11
  98. package/src/components/tasks/task-sheet.tsx +111 -103
  99. package/src/features/agents/queries.ts +20 -0
  100. package/src/features/chatrooms/queries.ts +20 -0
  101. package/src/features/chats/queries.ts +27 -0
  102. package/src/features/connectors/queries.ts +145 -0
  103. package/src/features/credentials/queries.ts +37 -0
  104. package/src/features/extensions/queries.ts +26 -0
  105. package/src/features/external-agents/queries.ts +36 -0
  106. package/src/features/gateways/queries.ts +274 -0
  107. package/src/features/missions/queries.ts +23 -0
  108. package/src/features/projects/queries.ts +20 -0
  109. package/src/features/protocols/queries.ts +149 -0
  110. package/src/features/providers/queries.ts +142 -0
  111. package/src/features/settings/queries.ts +20 -0
  112. package/src/features/skills/queries.ts +182 -0
  113. package/src/features/tasks/queries.ts +189 -0
  114. package/src/hooks/use-ws.ts +3 -2
  115. package/src/lib/agent-provider-options.test.ts +152 -0
  116. package/src/lib/agent-provider-options.ts +84 -0
  117. package/src/lib/app/api-client.ts +2 -2
  118. package/src/lib/providers/index.test.ts +78 -0
  119. package/src/lib/providers/index.ts +13 -10
  120. package/src/lib/query/client.ts +17 -0
  121. package/src/lib/server/agents/agent-runtime-config.ts +6 -6
  122. package/src/lib/server/agents/agent-service.ts +429 -0
  123. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  124. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  125. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  126. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  127. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  128. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  129. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  130. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  131. package/src/lib/server/build-llm.ts +7 -15
  132. package/src/lib/server/capability-router.test.ts +70 -1
  133. package/src/lib/server/capability-router.ts +24 -99
  134. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  135. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  136. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  137. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  138. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  139. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  140. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  141. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  142. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  143. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  144. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  145. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  146. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  147. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  148. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  149. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  150. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  151. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  152. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  153. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  154. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  155. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  156. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  157. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  158. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  159. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  160. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  161. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  162. package/src/lib/server/chats/chat-session-service.ts +410 -0
  163. package/src/lib/server/connectors/access.ts +1 -1
  164. package/src/lib/server/connectors/commands.ts +7 -6
  165. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  166. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  167. package/src/lib/server/connectors/connector-service.ts +453 -0
  168. package/src/lib/server/connectors/delivery.ts +17 -12
  169. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  170. package/src/lib/server/connectors/media.ts +1 -1
  171. package/src/lib/server/connectors/response-media.ts +1 -1
  172. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  173. package/src/lib/server/connectors/session.ts +9 -7
  174. package/src/lib/server/connectors/voice-note.ts +2 -1
  175. package/src/lib/server/context-manager.ts +20 -1
  176. package/src/lib/server/cost.ts +2 -3
  177. package/src/lib/server/credentials/credential-repository.ts +43 -4
  178. package/src/lib/server/credentials/credential-service.ts +112 -0
  179. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  180. package/src/lib/server/daemon/controller.ts +577 -0
  181. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  182. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  183. package/src/lib/server/daemon/types.ts +101 -0
  184. package/src/lib/server/embeddings.ts +3 -9
  185. package/src/lib/server/eval/agent-regression.ts +3 -2
  186. package/src/lib/server/eval/runner.ts +2 -2
  187. package/src/lib/server/execution-brief.test.ts +167 -0
  188. package/src/lib/server/execution-brief.ts +295 -0
  189. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  190. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  191. package/src/lib/server/execution-engine/index.ts +35 -0
  192. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  193. package/src/lib/server/execution-engine/types.ts +33 -0
  194. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  195. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  196. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  197. package/src/lib/server/messages/message-repository.ts +330 -0
  198. package/src/lib/server/missions/mission-service/core.ts +8 -6
  199. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  200. package/src/lib/server/openclaw/doctor.ts +1 -1
  201. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  202. package/src/lib/server/openclaw/gateway.ts +5 -14
  203. package/src/lib/server/openclaw/health.ts +3 -11
  204. package/src/lib/server/openclaw/sync.ts +8 -6
  205. package/src/lib/server/persistence/storage-context.ts +3 -0
  206. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  207. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  208. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  209. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  210. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  211. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  212. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  213. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  214. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  215. package/src/lib/server/protocols/protocol-types.ts +10 -7
  216. package/src/lib/server/provider-endpoint.ts +7 -12
  217. package/src/lib/server/provider-model-discovery.ts +2 -11
  218. package/src/lib/server/query-expansion.ts +5 -6
  219. package/src/lib/server/run-context.test.ts +365 -0
  220. package/src/lib/server/run-context.ts +367 -0
  221. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  222. package/src/lib/server/runtime/queue/core.ts +61 -190
  223. package/src/lib/server/runtime/run-ledger.ts +8 -0
  224. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  225. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  226. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  227. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  228. package/src/lib/server/service-result.ts +16 -0
  229. package/src/lib/server/session-note.ts +2 -3
  230. package/src/lib/server/session-reset-policy.ts +4 -3
  231. package/src/lib/server/session-tools/connector.ts +9 -6
  232. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  233. package/src/lib/server/session-tools/crud.ts +162 -10
  234. package/src/lib/server/session-tools/delegate.ts +1 -1
  235. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  236. package/src/lib/server/session-tools/memory.ts +6 -4
  237. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  238. package/src/lib/server/session-tools/session-info.ts +119 -12
  239. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  240. package/src/lib/server/session-tools/skills.ts +15 -15
  241. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  242. package/src/lib/server/session-tools/subagent.ts +125 -7
  243. package/src/lib/server/session-tools/team-context.ts +4 -3
  244. package/src/lib/server/session-tools/wallet.ts +0 -58
  245. package/src/lib/server/sessions/session-lineage.ts +55 -0
  246. package/src/lib/server/sessions/session-repository.ts +2 -2
  247. package/src/lib/server/skills/learned-skills.ts +24 -23
  248. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  249. package/src/lib/server/skills/skill-repository.ts +136 -13
  250. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  251. package/src/lib/server/storage-normalization.test.ts +42 -215
  252. package/src/lib/server/storage-normalization.ts +98 -0
  253. package/src/lib/server/storage.ts +19 -0
  254. package/src/lib/server/structured-extract.ts +3 -14
  255. package/src/lib/server/tasks/task-followups.ts +16 -11
  256. package/src/lib/server/tasks/task-result.test.ts +25 -29
  257. package/src/lib/server/tasks/task-result.ts +5 -9
  258. package/src/lib/server/tasks/task-route-service.ts +449 -0
  259. package/src/lib/server/text-normalization.ts +41 -0
  260. package/src/lib/server/tool-planning.ts +6 -42
  261. package/src/lib/server/upload-path.ts +5 -0
  262. package/src/lib/server/working-state/extraction.ts +614 -0
  263. package/src/lib/server/working-state/normalization.ts +866 -0
  264. package/src/lib/server/working-state/prompt.ts +60 -0
  265. package/src/lib/server/working-state/repository.ts +38 -0
  266. package/src/lib/server/working-state/service.test.ts +253 -0
  267. package/src/lib/server/working-state/service.ts +293 -0
  268. package/src/lib/validation/schemas.ts +1 -0
  269. package/src/lib/ws-client.ts +3 -3
  270. package/src/stores/slices/task-slice.ts +1 -4
  271. package/src/stores/use-chatroom-store.ts +2 -2
  272. package/src/types/index.ts +288 -22
  273. package/src/views/settings/section-providers.tsx +2 -2
@@ -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)
@@ -0,0 +1,152 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import type { Credentials, ProviderConfig, ProviderInfo } from '@/types'
5
+ import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from './agent-provider-options'
6
+
7
+ test('buildAgentSelectableProviders includes enabled custom providers missing from the provider list', () => {
8
+ const providers: ProviderInfo[] = [
9
+ {
10
+ id: 'openai',
11
+ name: 'OpenAI',
12
+ models: ['gpt-5'],
13
+ defaultModels: ['gpt-5'],
14
+ supportsModelDiscovery: true,
15
+ requiresApiKey: true,
16
+ requiresEndpoint: false,
17
+ },
18
+ ]
19
+ const providerConfigs: ProviderConfig[] = [
20
+ {
21
+ id: 'custom-llama',
22
+ name: 'Llama.cpp',
23
+ type: 'custom',
24
+ baseUrl: 'http://localhost:8080/v1',
25
+ models: ['llama-3.1-70b'],
26
+ requiresApiKey: false,
27
+ credentialId: null,
28
+ isEnabled: true,
29
+ createdAt: 1,
30
+ updatedAt: 1,
31
+ },
32
+ ]
33
+
34
+ const result = buildAgentSelectableProviders(providers, providerConfigs)
35
+ assert.equal(result.some((provider) => provider.id === 'custom-llama'), true)
36
+ assert.equal(result.find((provider) => provider.id === 'custom-llama')?.name, 'Llama.cpp')
37
+ assert.equal(result.find((provider) => provider.id === 'custom-llama')?.supportsModelDiscovery, false)
38
+ })
39
+
40
+ test('buildAgentSelectableProviders prefers custom provider config metadata when ids overlap', () => {
41
+ const providers: ProviderInfo[] = [
42
+ {
43
+ id: 'custom-llama',
44
+ name: 'Stale Custom',
45
+ models: ['old-model'],
46
+ defaultModels: ['old-model'],
47
+ supportsModelDiscovery: false,
48
+ requiresApiKey: true,
49
+ requiresEndpoint: false,
50
+ defaultEndpoint: 'http://old.example/v1',
51
+ },
52
+ ]
53
+ const providerConfigs: ProviderConfig[] = [
54
+ {
55
+ id: 'custom-llama',
56
+ name: 'Fresh Custom',
57
+ type: 'custom',
58
+ baseUrl: 'http://localhost:8080/v1',
59
+ models: ['llama-3.1-70b'],
60
+ requiresApiKey: false,
61
+ credentialId: 'cred_custom',
62
+ isEnabled: true,
63
+ createdAt: 1,
64
+ updatedAt: 2,
65
+ },
66
+ ]
67
+
68
+ const result = buildAgentSelectableProviders(providers, providerConfigs)
69
+ assert.deepEqual(result, [
70
+ {
71
+ id: 'custom-llama',
72
+ name: 'Fresh Custom',
73
+ models: ['llama-3.1-70b'],
74
+ defaultModels: ['llama-3.1-70b'],
75
+ supportsModelDiscovery: false,
76
+ requiresApiKey: false,
77
+ optionalApiKey: false,
78
+ requiresEndpoint: true,
79
+ defaultEndpoint: 'http://localhost:8080/v1',
80
+ credentialId: 'cred_custom',
81
+ type: 'custom',
82
+ },
83
+ ])
84
+ })
85
+
86
+ test('buildAgentSelectableProviders hides built-in providers disabled in provider configs', () => {
87
+ const providers: ProviderInfo[] = [
88
+ {
89
+ id: 'openai',
90
+ name: 'OpenAI',
91
+ models: ['gpt-5'],
92
+ defaultModels: ['gpt-5'],
93
+ supportsModelDiscovery: true,
94
+ requiresApiKey: true,
95
+ requiresEndpoint: false,
96
+ },
97
+ {
98
+ id: 'anthropic',
99
+ name: 'Anthropic',
100
+ models: ['claude-sonnet-4-6'],
101
+ defaultModels: ['claude-sonnet-4-6'],
102
+ supportsModelDiscovery: true,
103
+ requiresApiKey: true,
104
+ requiresEndpoint: false,
105
+ },
106
+ ]
107
+ const providerConfigs: ProviderConfig[] = [
108
+ {
109
+ id: 'openai',
110
+ name: 'OpenAI',
111
+ type: 'builtin',
112
+ baseUrl: '',
113
+ models: [],
114
+ requiresApiKey: true,
115
+ credentialId: null,
116
+ isEnabled: false,
117
+ createdAt: 1,
118
+ updatedAt: 1,
119
+ },
120
+ ]
121
+
122
+ const result = buildAgentSelectableProviders(providers, providerConfigs)
123
+ assert.deepEqual(result.map((provider) => provider.id), ['anthropic'])
124
+ })
125
+
126
+ test('resolveAgentSelectableProviderCredentials includes linked provider-config credentials', () => {
127
+ const credentials: Credentials = {
128
+ cred_custom: {
129
+ id: 'cred_custom',
130
+ provider: 'OpenRouter',
131
+ name: 'OpenRouter key',
132
+ createdAt: 1,
133
+ },
134
+ }
135
+ const providerConfigs: ProviderConfig[] = [
136
+ {
137
+ id: 'custom-openrouter',
138
+ name: 'OpenRouter Custom',
139
+ type: 'custom',
140
+ baseUrl: 'https://openrouter.ai/api/v1',
141
+ models: ['openai/gpt-4.1'],
142
+ requiresApiKey: true,
143
+ credentialId: 'cred_custom',
144
+ isEnabled: true,
145
+ createdAt: 1,
146
+ updatedAt: 1,
147
+ },
148
+ ]
149
+
150
+ const result = resolveAgentSelectableProviderCredentials('custom-openrouter', credentials, providerConfigs)
151
+ assert.deepEqual(result.map((credential) => credential.id), ['cred_custom'])
152
+ })
@@ -0,0 +1,84 @@
1
+ import type { Credential, Credentials, ProviderConfig, ProviderInfo } from '@/types'
2
+
3
+ export interface AgentSelectableProvider {
4
+ id: string
5
+ name: string
6
+ models: string[]
7
+ defaultModels?: string[]
8
+ supportsModelDiscovery?: boolean
9
+ requiresApiKey: boolean
10
+ optionalApiKey?: boolean
11
+ requiresEndpoint: boolean
12
+ defaultEndpoint?: string
13
+ credentialId?: string | null
14
+ type: 'builtin' | 'custom'
15
+ }
16
+
17
+ function buildCustomProviderOption(config: ProviderConfig): AgentSelectableProvider {
18
+ return {
19
+ id: config.id,
20
+ name: config.name || 'Custom Provider',
21
+ models: Array.isArray(config.models) ? config.models : [],
22
+ defaultModels: Array.isArray(config.models) ? config.models : [],
23
+ supportsModelDiscovery: false,
24
+ requiresApiKey: config.requiresApiKey,
25
+ optionalApiKey: false,
26
+ requiresEndpoint: Boolean(config.baseUrl),
27
+ defaultEndpoint: config.baseUrl || undefined,
28
+ credentialId: config.credentialId ?? null,
29
+ type: 'custom',
30
+ }
31
+ }
32
+
33
+ export function buildAgentSelectableProviders(
34
+ providers: ProviderInfo[],
35
+ providerConfigs: ProviderConfig[],
36
+ ): AgentSelectableProvider[] {
37
+ const disabledBuiltinIds = new Set(
38
+ providerConfigs
39
+ .filter((config) => config.type === 'builtin' && config.isEnabled === false)
40
+ .map((config) => config.id),
41
+ )
42
+
43
+ const merged: AgentSelectableProvider[] = providers
44
+ .filter((provider) => !disabledBuiltinIds.has(String(provider.id)))
45
+ .map((provider) => ({
46
+ ...provider,
47
+ credentialId: null,
48
+ type: 'builtin' as const,
49
+ }))
50
+ const indexById = new Map(merged.map((provider, index) => [provider.id, index]))
51
+
52
+ for (const config of providerConfigs) {
53
+ if (config.type !== 'custom' || config.isEnabled === false) continue
54
+ const customProvider = buildCustomProviderOption(config)
55
+ const existingIndex = indexById.get(config.id)
56
+ if (existingIndex == null) {
57
+ indexById.set(config.id, merged.length)
58
+ merged.push(customProvider)
59
+ continue
60
+ }
61
+ merged[existingIndex] = customProvider
62
+ }
63
+
64
+ return merged
65
+ }
66
+
67
+ export function resolveAgentSelectableProviderCredentials(
68
+ providerId: string,
69
+ credentials: Credentials,
70
+ providerConfigs: ProviderConfig[],
71
+ ): Credential[] {
72
+ const matches = Object.values(credentials).filter((credential) => credential.provider === providerId)
73
+ const config = providerConfigs.find((candidate) => (
74
+ candidate.type === 'custom'
75
+ && candidate.id === providerId
76
+ && candidate.isEnabled !== false
77
+ ))
78
+ if (!config?.credentialId) return matches
79
+
80
+ const linkedCredential = credentials[config.credentialId]
81
+ if (!linkedCredential) return matches
82
+ if (matches.some((credential) => credential.id === linkedCredential.id)) return matches
83
+ return [...matches, linkedCredential]
84
+ }
@@ -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) || ''