@swarmclawai/swarmclaw 1.0.6 → 1.0.8

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 (252) hide show
  1. package/README.md +43 -13
  2. package/package.json +10 -10
  3. package/src/app/activity/page.tsx +2 -0
  4. package/src/app/api/agents/[id]/clone/route.ts +1 -1
  5. package/src/app/api/agents/[id]/route.ts +25 -11
  6. package/src/app/api/agents/route.ts +15 -14
  7. package/src/app/api/canvas/[sessionId]/route.ts +2 -2
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
  9. package/src/app/api/chats/[id]/route.ts +13 -3
  10. package/src/app/api/chats/route.ts +13 -3
  11. package/src/app/api/clawhub/install/route.test.ts +97 -0
  12. package/src/app/api/clawhub/install/route.ts +78 -4
  13. package/src/app/api/clawhub/preview/route.ts +59 -0
  14. package/src/app/api/clawhub/search/route.ts +2 -1
  15. package/src/app/api/connectors/[id]/access/route.ts +254 -0
  16. package/src/app/api/{plugins → extensions}/dependencies/route.ts +1 -4
  17. package/src/app/api/{plugins → extensions}/install/route.ts +2 -6
  18. package/src/app/api/{plugins → extensions}/marketplace/route.ts +6 -9
  19. package/src/app/api/{plugins → extensions}/route.ts +17 -13
  20. package/src/app/api/{plugins → extensions}/settings/route.ts +13 -6
  21. package/src/app/api/{plugins → extensions}/ui/route.ts +9 -9
  22. package/src/app/api/learned-skills/route.ts +24 -0
  23. package/src/app/api/projects/[id]/route.ts +5 -1
  24. package/src/app/api/schedules/[id]/route.ts +71 -8
  25. package/src/app/api/schedules/[id]/run/route.ts +14 -2
  26. package/src/app/api/schedules/route.ts +20 -3
  27. package/src/app/api/search/route.ts +2 -2
  28. package/src/app/api/tasks/[id]/route.ts +8 -1
  29. package/src/app/api/tasks/route.ts +2 -1
  30. package/src/app/api/webhooks/[id]/helpers.ts +5 -5
  31. package/src/app/{plugins → extensions}/layout.tsx +3 -3
  32. package/src/app/{plugins → extensions}/page.tsx +3 -3
  33. package/src/app/home/page.tsx +7 -2
  34. package/src/app/inbox/page.tsx +7 -0
  35. package/src/app/layout.tsx +1 -1
  36. package/src/app/schedules/page.tsx +2 -2
  37. package/src/app/settings/page.tsx +1 -10
  38. package/src/app/skills/page.tsx +2 -22
  39. package/src/app/tasks/page.tsx +8 -2
  40. package/src/cli/index.js +22 -23
  41. package/src/cli/index.ts +13 -6
  42. package/src/cli/spec.js +18 -17
  43. package/src/components/agents/agent-card.tsx +3 -2
  44. package/src/components/agents/agent-chat-list.tsx +8 -5
  45. package/src/components/agents/agent-list.tsx +8 -5
  46. package/src/components/agents/agent-sheet.tsx +599 -737
  47. package/src/components/agents/inspector-panel.tsx +6 -5
  48. package/src/components/auth/setup-wizard/index.tsx +13 -7
  49. package/src/components/auth/setup-wizard/step-agents.tsx +2 -2
  50. package/src/components/auth/setup-wizard/types.ts +3 -1
  51. package/src/components/auth/setup-wizard/utils.ts +3 -1
  52. package/src/components/chat/chat-area.tsx +6 -5
  53. package/src/components/chat/chat-card.tsx +2 -1
  54. package/src/components/chat/chat-header.tsx +7 -6
  55. package/src/components/chat/chat-tool-toggles.tsx +9 -4
  56. package/src/components/chat/message-list.tsx +1 -1
  57. package/src/components/chat/swarm-panel.test.ts +1 -1
  58. package/src/components/chat/swarm-panel.tsx +1 -1
  59. package/src/components/chat/tool-events-section.test.ts +25 -0
  60. package/src/components/chat/tool-events-section.tsx +29 -19
  61. package/src/components/chat/tool-request-banner.tsx +6 -5
  62. package/src/components/chatrooms/agent-hover-card.tsx +4 -3
  63. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +3 -2
  64. package/src/components/connectors/connector-access-panel.tsx +416 -0
  65. package/src/components/connectors/connector-inbox.tsx +880 -0
  66. package/src/components/connectors/connector-sheet.tsx +368 -120
  67. package/src/components/input/chat-input.tsx +9 -7
  68. package/src/components/layout/dashboard-shell.tsx +23 -18
  69. package/src/components/layout/mobile-drawer.tsx +5 -5
  70. package/src/components/layout/sidebar-rail.tsx +9 -1
  71. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  72. package/src/components/plugins/plugin-list.tsx +25 -45
  73. package/src/components/plugins/plugin-sheet.tsx +41 -42
  74. package/src/components/schedules/schedule-console.tsx +596 -0
  75. package/src/components/schedules/schedule-list.tsx +4 -1
  76. package/src/components/schedules/schedule-sheet.tsx +6 -6
  77. package/src/components/settings/gateway-disconnect-overlay.tsx +13 -2
  78. package/src/components/shared/advanced-settings-section.tsx +59 -0
  79. package/src/components/shared/agent-picker-list.tsx +3 -3
  80. package/src/components/shared/bottom-sheet.tsx +1 -1
  81. package/src/components/skills/skill-list.tsx +172 -512
  82. package/src/components/skills/skills-workspace.tsx +1715 -0
  83. package/src/components/tasks/task-card.tsx +9 -3
  84. package/src/components/tasks/task-column.tsx +1 -0
  85. package/src/components/tasks/task-list.tsx +3 -2
  86. package/src/components/ui/dialog.tsx +1 -1
  87. package/src/components/ui/sheet.tsx +1 -1
  88. package/src/hooks/use-app-bootstrap.ts +36 -21
  89. package/src/instrumentation.ts +3 -2
  90. package/src/lib/agent-default-tools.ts +24 -14
  91. package/src/lib/app/navigation.ts +2 -1
  92. package/src/lib/app/view-constants.ts +17 -9
  93. package/src/lib/capability-selection.test.ts +44 -0
  94. package/src/lib/capability-selection.ts +91 -0
  95. package/src/lib/chat/chats.ts +3 -2
  96. package/src/lib/chat/queued-message-queue.test.ts +41 -0
  97. package/src/lib/chat/queued-message-queue.ts +49 -0
  98. package/src/lib/connectors/sender-id.test.ts +32 -0
  99. package/src/lib/connectors/sender-id.ts +31 -0
  100. package/src/lib/observability/local-observability.test.ts +2 -0
  101. package/src/lib/observability/local-observability.ts +1 -1
  102. package/src/lib/plugin-install-cors.ts +1 -1
  103. package/src/lib/provider-sets.ts +1 -1
  104. package/src/lib/providers/claude-cli.ts +2 -1
  105. package/src/lib/runtime/runtime-loop.ts +0 -20
  106. package/src/lib/schedules/schedules.ts +31 -3
  107. package/src/lib/server/agents/agent-registry.ts +2 -2
  108. package/src/lib/server/agents/agent-runtime-config.test.ts +27 -0
  109. package/src/lib/server/agents/agent-runtime-config.ts +12 -11
  110. package/src/lib/server/agents/agent-thread-session.test.ts +76 -1
  111. package/src/lib/server/agents/agent-thread-session.ts +28 -4
  112. package/src/lib/server/agents/delegation-jobs-advanced.test.ts +4 -4
  113. package/src/lib/server/agents/main-agent-loop-advanced.test.ts +43 -8
  114. package/src/lib/server/agents/main-agent-loop.ts +22 -15
  115. package/src/lib/server/agents/subagent-runtime.ts +23 -29
  116. package/src/lib/server/agents/subagent-swarm.ts +1 -1
  117. package/src/lib/server/agents/task-session.ts +62 -0
  118. package/src/lib/server/autonomy/supervisor-reflection.test.ts +14 -12
  119. package/src/lib/server/autonomy/supervisor-reflection.ts +2 -4
  120. package/src/lib/server/build-llm.test.ts +250 -105
  121. package/src/lib/server/build-llm.ts +146 -73
  122. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +50 -0
  123. package/src/lib/server/chat-execution/chat-execution-connector-delivery.ts +22 -3
  124. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +186 -0
  125. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +10 -0
  126. package/src/lib/server/chat-execution/chat-execution-utils.ts +45 -3
  127. package/src/lib/server/chat-execution/chat-execution.ts +128 -127
  128. package/src/lib/server/chat-execution/chat-streaming-utils.ts +57 -1
  129. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +8 -2
  130. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +55 -1
  131. package/src/lib/server/chat-execution/stream-agent-chat.ts +76 -43
  132. package/src/lib/server/chatrooms/chatroom-helpers.test.ts +29 -0
  133. package/src/lib/server/chatrooms/chatroom-helpers.ts +23 -18
  134. package/src/lib/server/chatrooms/session-mailbox.test.ts +61 -0
  135. package/src/lib/server/chatrooms/session-mailbox.ts +128 -3
  136. package/src/lib/server/connectors/access.test.ts +146 -0
  137. package/src/lib/server/connectors/access.ts +231 -29
  138. package/src/lib/server/connectors/commands.ts +2 -1
  139. package/src/lib/server/connectors/manager-roundtrip.test.ts +1 -2
  140. package/src/lib/server/connectors/manager.test.ts +1082 -161
  141. package/src/lib/server/connectors/manager.ts +87 -86
  142. package/src/lib/server/connectors/outbox.ts +5 -1
  143. package/src/lib/server/connectors/pairing.test.ts +29 -0
  144. package/src/lib/server/connectors/pairing.ts +162 -35
  145. package/src/lib/server/connectors/policy.ts +16 -1
  146. package/src/lib/server/connectors/session-consolidation.ts +42 -3
  147. package/src/lib/server/connectors/session.test.ts +134 -0
  148. package/src/lib/server/connectors/session.ts +36 -2
  149. package/src/lib/server/connectors/types.ts +2 -0
  150. package/src/lib/server/connectors/whatsapp.test.ts +22 -2
  151. package/src/lib/server/connectors/whatsapp.ts +70 -1
  152. package/src/lib/server/cost.ts +1 -1
  153. package/src/lib/server/embeddings.ts +1 -1
  154. package/src/lib/server/eval/agent-regression-advanced.test.ts +2 -2
  155. package/src/lib/server/eval/agent-regression.test.ts +2 -2
  156. package/src/lib/server/eval/agent-regression.ts +56 -44
  157. package/src/lib/server/eval/runner.ts +1 -1
  158. package/src/lib/server/knowledge-db.test.ts +1 -1
  159. package/src/lib/server/memory/memory-consolidation.ts +2 -2
  160. package/src/lib/server/memory/session-memory-scope.test.ts +65 -0
  161. package/src/lib/server/memory/session-memory-scope.ts +47 -0
  162. package/src/lib/server/native-capabilities.test.ts +25 -0
  163. package/src/lib/server/native-capabilities.ts +681 -0
  164. package/src/lib/server/plugins.ts +3 -3
  165. package/src/lib/server/project-utils.ts +2 -2
  166. package/src/lib/server/provider-endpoint.ts +69 -0
  167. package/src/lib/server/runtime/daemon-state.ts +14 -13
  168. package/src/lib/server/runtime/heartbeat-service.ts +1 -1
  169. package/src/lib/server/runtime/heartbeat-wake.test.ts +26 -0
  170. package/src/lib/server/runtime/heartbeat-wake.ts +24 -3
  171. package/src/lib/server/runtime/queue.ts +78 -33
  172. package/src/lib/server/runtime/runtime-settings.test.ts +0 -8
  173. package/src/lib/server/runtime/runtime-settings.ts +0 -12
  174. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +1 -11
  175. package/src/lib/server/runtime/scheduler.test.ts +74 -0
  176. package/src/lib/server/runtime/scheduler.ts +32 -15
  177. package/src/lib/server/runtime/session-run-manager.ts +15 -7
  178. package/src/lib/server/runtime/watch-jobs.test.ts +24 -0
  179. package/src/lib/server/runtime/watch-jobs.ts +35 -2
  180. package/src/lib/server/schedules/schedule-lifecycle.test.ts +180 -0
  181. package/src/lib/server/schedules/schedule-lifecycle.ts +393 -0
  182. package/src/lib/server/schedules/schedule-normalization.test.ts +35 -1
  183. package/src/lib/server/schedules/schedule-normalization.ts +60 -8
  184. package/src/lib/server/schedules/schedule-service.ts +2 -1
  185. package/src/lib/server/session-tools/canvas.ts +5 -5
  186. package/src/lib/server/session-tools/chatroom.ts +2 -2
  187. package/src/lib/server/session-tools/connector.test.ts +85 -10
  188. package/src/lib/server/session-tools/connector.ts +269 -50
  189. package/src/lib/server/session-tools/context-mgmt.ts +9 -3
  190. package/src/lib/server/session-tools/context.ts +3 -1
  191. package/src/lib/server/session-tools/crawl.ts +2 -1
  192. package/src/lib/server/session-tools/crud.test.ts +5 -3
  193. package/src/lib/server/session-tools/crud.ts +40 -17
  194. package/src/lib/server/session-tools/delegate-fallback.test.ts +8 -8
  195. package/src/lib/server/session-tools/delegate.ts +21 -7
  196. package/src/lib/server/session-tools/discovery-approvals.test.ts +34 -15
  197. package/src/lib/server/session-tools/discovery.ts +55 -26
  198. package/src/lib/server/session-tools/extract.ts +2 -1
  199. package/src/lib/server/session-tools/human-loop.ts +31 -7
  200. package/src/lib/server/session-tools/index.ts +34 -9
  201. package/src/lib/server/session-tools/manage-connectors.test.ts +2 -2
  202. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +72 -3
  203. package/src/lib/server/session-tools/manage-schedules.test.ts +152 -8
  204. package/src/lib/server/session-tools/manage-skills.test.ts +4 -2
  205. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +3 -3
  206. package/src/lib/server/session-tools/manage-tasks.test.ts +1 -1
  207. package/src/lib/server/session-tools/memory.ts +20 -11
  208. package/src/lib/server/session-tools/monitor.ts +2 -2
  209. package/src/lib/server/session-tools/openclaw-workspace.ts +2 -2
  210. package/src/lib/server/session-tools/platform.ts +4 -5
  211. package/src/lib/server/session-tools/plugin-creator.ts +4 -3
  212. package/src/lib/server/session-tools/primitive-tools.test.ts +15 -2
  213. package/src/lib/server/session-tools/schedule.ts +2 -2
  214. package/src/lib/server/session-tools/session-info.ts +8 -4
  215. package/src/lib/server/session-tools/skill-runtime.test.ts +6 -2
  216. package/src/lib/server/session-tools/skill-runtime.ts +3 -0
  217. package/src/lib/server/session-tools/skills.ts +14 -1
  218. package/src/lib/server/session-tools/subagent.test.ts +50 -0
  219. package/src/lib/server/session-tools/subagent.ts +26 -4
  220. package/src/lib/server/session-tools/wallet.ts +2 -2
  221. package/src/lib/server/skills/clawhub-client.test.ts +109 -19
  222. package/src/lib/server/skills/clawhub-client.ts +115 -23
  223. package/src/lib/server/skills/learned-skills.test.ts +336 -0
  224. package/src/lib/server/skills/learned-skills.ts +719 -0
  225. package/src/lib/server/skills/runtime-skill-resolver.test.ts +70 -1
  226. package/src/lib/server/skills/runtime-skill-resolver.ts +71 -2
  227. package/src/lib/server/skills/skill-discovery.ts +1 -1
  228. package/src/lib/server/skills/skill-suggestions.ts +69 -7
  229. package/src/lib/server/storage.ts +325 -54
  230. package/src/lib/server/tasks/task-followups.test.ts +114 -0
  231. package/src/lib/server/tasks/task-followups.ts +78 -31
  232. package/src/lib/server/tasks/task-service.ts +1 -0
  233. package/src/lib/server/tool-planning.ts +7 -1
  234. package/src/lib/setup-defaults.ts +5 -5
  235. package/src/lib/validation/schemas.test.ts +21 -7
  236. package/src/lib/validation/schemas.ts +6 -6
  237. package/src/stores/slices/data-slice.ts +13 -13
  238. package/src/stores/use-app-store.test.ts +11 -0
  239. package/src/stores/use-chat-store.test.ts +151 -0
  240. package/src/stores/use-chat-store.ts +87 -37
  241. package/src/types/index.ts +137 -22
  242. package/src/views/settings/plugin-manager.tsx +27 -27
  243. package/src/views/settings/section-capability-policy.tsx +3 -3
  244. package/src/views/settings/section-embedding.tsx +25 -12
  245. package/src/views/settings/section-runtime-loop.tsx +1 -33
  246. package/src/app/api/orchestrator/graph/route.ts +0 -25
  247. package/src/app/api/orchestrator/run/route.ts +0 -34
  248. package/src/lib/server/agents/orchestrator-lg-structure.test.ts +0 -17
  249. package/src/lib/server/agents/orchestrator-lg.ts +0 -636
  250. package/src/lib/server/agents/orchestrator.ts +0 -409
  251. package/src/views/settings/section-orchestrator.tsx +0 -141
  252. /package/src/lib/server/chatrooms/{chatroom-orchestration.ts → chatroom-agent-signals.ts} +0 -0
package/README.md CHANGED
@@ -8,22 +8,31 @@
8
8
  <img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
9
9
  </p>
10
10
 
11
- SwarmClaw is a self-hosted AI orchestration runtime for OpenClaw and multi-agent work. It helps you run autonomous agents with heartbeats, schedules, delegation, memory, runtime skills, and reviewed conversation-to-skill learning across OpenClaw gateways and other providers.
11
+ SwarmClaw is a self-hosted AI runtime for OpenClaw and multi-agent work. It helps you run autonomous agents with heartbeats, schedules, delegation, memory, runtime skills, and reviewed conversation-to-skill learning across OpenClaw gateways and other providers.
12
12
 
13
13
  GitHub: https://github.com/swarmclawai/swarmclaw
14
14
  Docs: https://swarmclaw.ai/docs
15
15
  Website: https://swarmclaw.ai
16
- Plugin tutorial: https://swarmclaw.ai/docs/plugin-tutorial
16
+ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
17
+
18
+ ## Release Notes
19
+
20
+ ### v1.0.8 Highlights
21
+
22
+ - **Learned skills and self-healing**: SwarmClaw now keeps agent-scoped learned skills and shadow revisions so repeated successes and repeated external integration failures can harden into reusable local behavior without silently mutating the shared skill library.
23
+ - **Direct chat stability**: chat-origin runs now stop after the visible answer instead of enqueueing hidden follow-up loops, leaked control tokens such as `NO_MESSAGE` stay out of the user transcript, and repeated internal reruns no longer replace the reply the user already saw.
24
+ - **OpenClaw and Ollama route hardening**: agent thread sessions now repair stale credential/endpoint resolution more aggressively, including Ollama Cloud vs local endpoint selection and OpenClaw gateway fallback behavior.
25
+ - **Operator UX fixes**: new agents appear in the list immediately, gateway-disconnected chat CTAs now route to the current agent's settings instead of global settings, setup-wizard flicker after access-key login is gone, and screenshot-heavy tool runs no longer render duplicate previews in chat.
17
26
 
18
27
  ## What SwarmClaw Focuses On
19
28
 
20
- - **AI orchestration**: LangGraph-backed orchestration, delegated work, subagents, durable jobs, checkpointing, and background task execution.
21
- - **Autonomy and memory**: heartbeats, schedules, long-running execution, durable memory, working memory, document recall, and project-aware context.
29
+ - **Delegation and background execution**: delegated work, subagents, durable jobs, checkpointing, and background task execution.
30
+ - **Autonomy and memory**: heartbeats, schedules, long-running execution, durable memory, reflection memory, human-context learning, document recall, and project-aware context.
22
31
  - **OpenClaw integration**: named gateway profiles, external runtimes, deploy helpers, config sync, approval handling, and OpenClaw agent file editing.
23
32
  - **Runtime skills**: pinned skills, OpenClaw-compatible `SKILL.md` import, on-demand skill execution, and configurable keyword or embedding-based recommendation.
24
33
  - **Conversation-to-skill drafts**: draft a reusable skill from a real chat, review it, then approve it into the skill library.
25
34
  - **Crypto wallets**: agent-linked Solana and Ethereum wallets for balances, approvals, signing, simulation, and execution.
26
- - **Operator tooling**: connectors, plugins, browser automation, shell/files/git tooling, and runtime guardrails.
35
+ - **Operator tooling**: connectors, extensions, browser automation, shell/files/git tooling, and runtime guardrails.
27
36
 
28
37
  ## OpenClaw
29
38
 
@@ -45,14 +54,23 @@ npm i -g @swarmclawai/swarmclaw
45
54
  swarmclaw
46
55
  ```
47
56
 
48
- Running `swarmclaw` starts the server on `http://localhost:3456`.
57
+ ```bash
58
+ yarn global add @swarmclawai/swarmclaw
59
+ swarmclaw
60
+ ```
49
61
 
50
- ### One-off run
62
+ ```bash
63
+ pnpm add -g @swarmclawai/swarmclaw
64
+ swarmclaw
65
+ ```
51
66
 
52
67
  ```bash
53
- npx @swarmclawai/swarmclaw
68
+ bun add -g @swarmclawai/swarmclaw
69
+ swarmclaw
54
70
  ```
55
71
 
72
+ Running `swarmclaw` starts the server on `http://localhost:3456`.
73
+
56
74
  ### From the repo
57
75
 
58
76
  ```bash
@@ -63,12 +81,25 @@ npm run quickstart
63
81
 
64
82
  `npm run quickstart` installs dependencies, prepares local config and runtime state, and starts SwarmClaw.
65
83
 
84
+ ### Docker
85
+
86
+ ```bash
87
+ git clone https://github.com/swarmclawai/swarmclaw.git
88
+ cd swarmclaw
89
+ mkdir -p data
90
+ touch .env.local
91
+ docker compose up -d --build
92
+ ```
93
+
94
+ Then open `http://localhost:3456`.
95
+
66
96
  ## Skill Drafts From Conversations
67
97
 
68
98
  - From any active chat, use **Draft Skill** in the chat header.
69
99
  - Or open **Skills** and use **Draft From Current Chat**.
70
100
  - New agents keep **Conversation Skill Drafting** enabled by default, and you can switch it off per agent.
71
101
  - SwarmClaw turns useful work into a **draft suggestion**, not a live self-modifying skill.
102
+ - Learned skills stay **user/agent scoped** by default. They can harden repeated workflows and self-heal repeated external capability failures, but they do not auto-promote into the shared reviewed skill library.
72
103
  - Review the suggested name, rationale, summary, and transcript snippet.
73
104
  - Approve it to save it into the normal skill library, or dismiss it.
74
105
  - Runtime skill recommendations can use **keyword** or **embedding** ranking from **Settings → Memory & AI → Skills**.
@@ -77,11 +108,11 @@ npm run quickstart
77
108
 
78
109
  - **Providers**: OpenClaw, OpenAI, Anthropic, Ollama, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, plus compatible custom endpoints.
79
110
  - **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
80
- - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, and agent wakeups.
81
- - **Memory**: hybrid recall, graph traversal, journaling, durable documents, and project-scoped context.
111
+ - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
112
+ - **Memory**: hybrid recall, graph traversal, journaling, durable documents, project-scoped context, automatic reflection memory, communication preferences, profile and boundary memory, significant events, and open follow-up loops.
82
113
  - **Wallets**: balances, transfers, signatures, EVM call/quote/swap flows, and approval-gated execution.
83
114
  - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, and more.
84
- - **Plugins**: tool plugins, UI extensions, hooks, install/update flows, and runtime policy controls.
115
+ - **Extensions**: external tool extensions, UI modules, hooks, and install/update flows.
85
116
 
86
117
  ## Requirements
87
118
 
@@ -102,7 +133,6 @@ npm run quickstart
102
133
  - Getting started: https://swarmclaw.ai/docs/getting-started
103
134
  - OpenClaw setup: https://swarmclaw.ai/docs/openclaw-setup
104
135
  - Agents: https://swarmclaw.ai/docs/agents
105
- - Orchestration: https://swarmclaw.ai/docs/orchestration
106
136
  - Connectors: https://swarmclaw.ai/docs/connectors
107
- - Plugins: https://swarmclaw.ai/docs/plugins
137
+ - Extensions: https://swarmclaw.ai/docs/extensions
108
138
  - CLI reference: https://swarmclaw.ai/docs/cli
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.0.6",
4
- "description": "Self-hosted AI orchestration control plane for OpenClaw, multi-agent workflows, runtime skills, crypto wallets, and chat platform connectors.",
3
+ "version": "1.0.8",
4
+ "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -12,15 +12,15 @@
12
12
  "url": "git+https://github.com/swarmclawai/swarmclaw.git"
13
13
  },
14
14
  "keywords": [
15
- "ai",
16
- "agents",
17
- "llm",
18
- "orchestration",
19
- "swarm",
20
15
  "openclaw",
21
- "self-hosted",
22
- "wallets",
23
- "multi-agent"
16
+ "crustacean",
17
+ "clawd",
18
+ "clawdbot",
19
+ "moltbot",
20
+ "openclaw-skill",
21
+ "openclaw-dashboard",
22
+ "openclaw-gateway",
23
+ "clawdbot-dashboard"
24
24
  ],
25
25
  "engines": {
26
26
  "node": ">=22.6.0"
@@ -21,6 +21,8 @@ const ACTION_COLORS: Record<string, string> = {
21
21
  queued: 'bg-amber-500/15 text-amber-400',
22
22
  completed: 'bg-emerald-500/15 text-emerald-400',
23
23
  failed: 'bg-red-500/15 text-red-400',
24
+ archived: 'bg-white/[0.06] text-text-3',
25
+ restored: 'bg-sky-500/15 text-sky-400',
24
26
  approved: 'bg-green-500/15 text-green-400',
25
27
  rejected: 'bg-red-500/15 text-red-400',
26
28
  }
@@ -14,7 +14,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
14
14
  const now = Date.now()
15
15
 
16
16
  // Deep-copy the source agent, then override clone-specific fields
17
- const cloned = JSON.parse(JSON.stringify(source)) as Record<string, unknown>
17
+ const cloned = JSON.parse(JSON.stringify(source)) as typeof source
18
18
  cloned.id = newId
19
19
  cloned.name = `${source.name} (Copy)`
20
20
  cloned.createdAt = now
@@ -6,6 +6,7 @@ import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-sessi
6
6
  import { suspendAgentReferences } from '@/lib/server/agents/agent-cascade'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
9
+ import { normalizeCapabilitySelection } from '@/lib/capability-selection'
9
10
 
10
11
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
12
  const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents', table: 'agents' }
@@ -15,15 +16,27 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
15
16
  const body = await req.json()
16
17
  const result = mutateItem(ops, id, (agent) => {
17
18
  Object.assign(agent, body, { updatedAt: Date.now() })
18
- if (Array.isArray(body.plugins) || Array.isArray(body.tools)) {
19
- agent.plugins = Array.isArray(body.plugins) ? body.plugins : body.tools
20
- delete (agent as Record<string, unknown>).tools
19
+ if (body.tools !== undefined || body.extensions !== undefined) {
20
+ const nextSelection = normalizeCapabilitySelection({
21
+ tools: Array.isArray(body.tools) ? body.tools : agent.tools,
22
+ extensions: Array.isArray(body.extensions) ? body.extensions : agent.extensions,
23
+ })
24
+ agent.tools = nextSelection.tools
25
+ agent.extensions = nextSelection.extensions
21
26
  }
22
- if (body.platformAssignScope === 'all' || body.platformAssignScope === 'self') {
23
- agent.platformAssignScope = body.platformAssignScope
24
- agent.isOrchestrator = body.platformAssignScope === 'all'
25
- } else if (agent.platformAssignScope === 'all' || agent.platformAssignScope === 'self') {
26
- agent.isOrchestrator = agent.platformAssignScope === 'all'
27
+ if (body.delegationEnabled !== undefined) {
28
+ agent.delegationEnabled = body.delegationEnabled === true
29
+ }
30
+ if (body.delegationTargetMode === 'all' || body.delegationTargetMode === 'selected') {
31
+ agent.delegationTargetMode = body.delegationTargetMode
32
+ }
33
+ if (body.delegationTargetAgentIds !== undefined) {
34
+ agent.delegationTargetAgentIds = Array.isArray(body.delegationTargetAgentIds)
35
+ ? body.delegationTargetAgentIds.filter((entry: unknown): entry is string => typeof entry === 'string' && entry.trim().length > 0)
36
+ : []
37
+ }
38
+ if (agent.delegationTargetMode !== 'selected') {
39
+ agent.delegationTargetAgentIds = []
27
40
  }
28
41
  if (body.apiEndpoint !== undefined) {
29
42
  agent.apiEndpoint = normalizeProviderEndpoint(
@@ -67,8 +80,9 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
67
80
  priority: typeof target.priority === 'number' ? target.priority : index + 1,
68
81
  }))
69
82
  }
83
+ delete (agent as Record<string, unknown>).platformAssignScope
84
+ delete (agent as Record<string, unknown>).subAgentIds
70
85
  delete (agent as Record<string, unknown>).isOrchestrator
71
- agent.isOrchestrator = agent.platformAssignScope === 'all'
72
86
  delete (agent as Record<string, unknown>).id
73
87
  agent.id = id
74
88
  return agent
@@ -113,11 +127,11 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
113
127
  // Detach sessions from the trashed agent
114
128
  const sessions = loadSessions()
115
129
  const detached: Array<[string, unknown]> = []
116
- for (const session of Object.values(sessions) as Array<Record<string, unknown>>) {
130
+ for (const session of Object.values(sessions)) {
117
131
  if (!session || session.agentId !== id) continue
118
132
  session.agentId = null
119
133
  session.heartbeatEnabled = false
120
- detached.push([session.id as string, session])
134
+ detached.push([session.id, session])
121
135
  }
122
136
  if (detached.length > 0) {
123
137
  upsertStoredItems('sessions', detached)
@@ -5,7 +5,7 @@ import { loadAgents, loadSessions, loadUsage, logActivity, upsertStoredItem } fr
5
5
  import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
6
6
  import { notify } from '@/lib/server/ws-hub'
7
7
  import { getAgentSpendWindows } from '@/lib/server/cost'
8
- import { resolveAgentPluginSelection } from '@/lib/agent-default-tools'
8
+ import { resolveAgentToolSelection } from '@/lib/agent-default-tools'
9
9
  import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
10
10
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
11
11
  import { z } from 'zod'
@@ -22,9 +22,6 @@ export async function GET(req: Request) {
22
22
  const agents = loadAgents()
23
23
  const sessions = loadSessions()
24
24
  const usage = loadUsage()
25
- for (const agent of Object.values(agents)) {
26
- agent.isOrchestrator = agent.platformAssignScope === 'all'
27
- }
28
25
  // Enrich agents that have spend limits with current spend windows
29
26
  for (const agent of Object.values(agents)) {
30
27
  if (
@@ -32,7 +29,10 @@ export async function GET(req: Request) {
32
29
  || (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0)
33
30
  || (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0)
34
31
  ) {
35
- const spend = getAgentSpendWindows(agent.id, Date.now(), { sessions, usage })
32
+ const spend = getAgentSpendWindows(agent.id, Date.now(), {
33
+ sessions: sessions as unknown as Record<string, Record<string, unknown>>,
34
+ usage,
35
+ })
36
36
  if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = spend.monthly
37
37
  if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = spend.daily
38
38
  if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = spend.hourly
@@ -63,15 +63,14 @@ export async function POST(req: Request) {
63
63
  return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
64
64
  }
65
65
  const body = parsed.data
66
- const plugins = resolveAgentPluginSelection({
67
- hasExplicitPlugins: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'plugins')),
66
+ const capabilitySelection = resolveAgentToolSelection({
68
67
  hasExplicitTools: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'tools')),
69
- plugins: body.plugins,
68
+ hasExplicitExtensions: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'extensions')),
70
69
  tools: body.tools,
70
+ extensions: body.extensions,
71
71
  })
72
72
  const id = genId()
73
73
  const now = Date.now()
74
- const platformAssignScope = body.platformAssignScope
75
74
  const agent = {
76
75
  id,
77
76
  name: body.name,
@@ -91,10 +90,11 @@ export async function POST(req: Request) {
91
90
  ...target,
92
91
  apiEndpoint: normalizeProviderEndpoint(target.provider, target.apiEndpoint || null),
93
92
  })),
94
- isOrchestrator: platformAssignScope === 'all',
95
- platformAssignScope,
96
- subAgentIds: body.subAgentIds,
97
- plugins,
93
+ delegationEnabled: body.delegationEnabled ?? false,
94
+ delegationTargetMode: body.delegationTargetMode ?? 'all',
95
+ delegationTargetAgentIds: (body.delegationTargetMode === 'selected' ? body.delegationTargetAgentIds : []).filter(Boolean),
96
+ tools: capabilitySelection.tools,
97
+ extensions: capabilitySelection.extensions,
98
98
  skills: body.skills,
99
99
  skillIds: body.skillIds,
100
100
  mcpServerIds: body.mcpServerIds,
@@ -103,7 +103,7 @@ export async function POST(req: Request) {
103
103
  thinkingLevel: body.thinkingLevel || undefined,
104
104
  autoRecovery: body.autoRecovery || false,
105
105
  disabled: body.disabled || false,
106
- heartbeatEnabled: body.heartbeatEnabled || false,
106
+ heartbeatEnabled: body.heartbeatEnabled ?? true,
107
107
  heartbeatInterval: body.heartbeatInterval,
108
108
  heartbeatIntervalSec: body.heartbeatIntervalSec,
109
109
  heartbeatModel: body.heartbeatModel,
@@ -116,6 +116,7 @@ export async function POST(req: Request) {
116
116
  identityState: body.identityState ?? null,
117
117
  memoryScopeMode: body.memoryScopeMode,
118
118
  memoryTierPreference: body.memoryTierPreference,
119
+ proactiveMemory: body.proactiveMemory ?? true,
119
120
  autoDraftSkillSuggestions: body.autoDraftSkillSuggestions,
120
121
  projectId: body.projectId,
121
122
  avatarSeed: body.avatarSeed,
@@ -11,7 +11,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ session
11
11
 
12
12
  return NextResponse.json({
13
13
  sessionId,
14
- content: (session as Record<string, unknown>).canvasContent || null,
14
+ content: (session as unknown as Record<string, unknown>).canvasContent || null,
15
15
  })
16
16
  }
17
17
 
@@ -23,7 +23,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ session
23
23
  if (!session) return NextResponse.json({ error: 'Session not found' }, { status: 404 })
24
24
 
25
25
  const nextContent = normalizeCanvasContent(body.document ?? body.content)
26
- ;(session as Record<string, unknown>).canvasContent = nextContent
26
+ ;(session as unknown as Record<string, unknown>).canvasContent = nextContent
27
27
  session.lastActiveAt = Date.now()
28
28
  sessions[sessionId] = session
29
29
  saveSessions(sessions)
@@ -21,7 +21,7 @@ import {
21
21
  import { filterHealthyChatroomAgents } from '@/lib/server/chatrooms/chatroom-health'
22
22
  import { evaluateRoutingRules } from '@/lib/server/chatrooms/chatroom-routing'
23
23
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
24
- import { applyAgentReactionsFromText } from '@/lib/server/chatrooms/chatroom-orchestration'
24
+ import { applyAgentReactionsFromText } from '@/lib/server/chatrooms/chatroom-agent-signals'
25
25
  import { resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
26
26
  import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
27
27
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
@@ -5,7 +5,7 @@ import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
5
5
  import { resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
6
6
  import { clearMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
7
7
  import { getSessionRunState } from '@/lib/server/runtime/session-run-manager'
8
- import type { Session } from '@/types'
8
+ import { normalizeCapabilitySelection } from '@/lib/capability-selection'
9
9
 
10
10
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
11
11
  const { id } = await params
@@ -79,8 +79,18 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
79
79
  session.routePreferredGatewayUseCase = routePreferredGatewayUseCase
80
80
  }
81
81
 
82
- if (updates.plugins !== undefined) session.plugins = updates.plugins
83
- else if (agentIdUpdateProvided && linkedAgent) session.plugins = Array.isArray(linkedAgent.plugins) ? linkedAgent.plugins : []
82
+ if (updates.tools !== undefined || updates.extensions !== undefined || (agentIdUpdateProvided && linkedAgent)) {
83
+ const nextSelection = normalizeCapabilitySelection({
84
+ tools: Array.isArray(updates.tools)
85
+ ? updates.tools
86
+ : (agentIdUpdateProvided && linkedAgent ? linkedAgent.tools : session.tools as string[] | undefined),
87
+ extensions: Array.isArray(updates.extensions)
88
+ ? updates.extensions
89
+ : (agentIdUpdateProvided && linkedAgent ? linkedAgent.extensions : session.extensions as string[] | undefined),
90
+ })
91
+ session.tools = nextSelection.tools
92
+ session.extensions = nextSelection.extensions
93
+ }
84
94
 
85
95
  if (updates.apiEndpoint !== undefined) {
86
96
  session.apiEndpoint = normalizeProviderEndpoint(
@@ -12,6 +12,7 @@ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent
12
12
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
13
13
  import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
14
14
  import { buildSessionListSummary } from '@/lib/chat/session-summary'
15
+ import { normalizeCapabilitySelection } from '@/lib/capability-selection'
15
16
  export const dynamic = 'force-dynamic'
16
17
 
17
18
  async function ensureDaemonIfNeeded(source: string) {
@@ -22,6 +23,12 @@ async function ensureDaemonIfNeeded(source: string) {
22
23
 
23
24
  export async function GET(req: Request) {
24
25
  const endPerf = perf.start('api', 'GET /api/chats')
26
+ try {
27
+ const { pruneThreadConnectorMirrors } = await import('@/lib/server/connectors/session-consolidation')
28
+ pruneThreadConnectorMirrors()
29
+ } catch (err) {
30
+ console.error('[api/chats] pruneThreadConnectorMirrors failed:', err)
31
+ }
25
32
  const sessions = loadSessions()
26
33
  const changedSessionIds: string[] = []
27
34
  for (const id of Object.keys(sessions)) {
@@ -105,8 +112,10 @@ export async function POST(req: Request) {
105
112
  preferredGatewayTags: routePreferredGatewayTags,
106
113
  preferredGatewayUseCase: routePreferredGatewayUseCase,
107
114
  }) : null
108
- const requestedPlugins = Array.isArray(body.plugins) ? body.plugins : (Array.isArray(body.tools) ? body.tools : null)
109
- const resolvedPlugins = requestedPlugins ?? (Array.isArray(agent?.plugins) ? agent.plugins : (Array.isArray(agent?.tools) ? agent.tools : []))
115
+ const resolvedCapabilities = normalizeCapabilitySelection({
116
+ tools: Array.isArray(body.tools) ? body.tools : agent?.tools,
117
+ extensions: Array.isArray(body.extensions) ? body.extensions : agent?.extensions,
118
+ })
110
119
 
111
120
  // If session with this ID already exists, return it as-is
112
121
  if (body.id && sessions[id]) {
@@ -142,7 +151,8 @@ export async function POST(req: Request) {
142
151
  sessionType: body.sessionType || 'human',
143
152
  agentId: body.agentId || null,
144
153
  parentSessionId: body.parentSessionId || null,
145
- plugins: resolvedPlugins,
154
+ tools: resolvedCapabilities.tools,
155
+ extensions: resolvedCapabilities.extensions,
146
156
  heartbeatEnabled: body.heartbeatEnabled ?? null,
147
157
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
148
158
  sessionResetMode: body.sessionResetMode ?? agent?.sessionResetMode ?? null,
@@ -0,0 +1,97 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-clawhub-install-route-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ SWARMCLAW_HOME: path.join(tempDir, 'swarmclaw-home'),
20
+ },
21
+ encoding: 'utf-8',
22
+ })
23
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
+ const lines = (result.stdout || '')
25
+ .trim()
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
+ return JSON.parse(jsonLine || '{}')
31
+ } finally {
32
+ fs.rmSync(tempDir, { recursive: true, force: true })
33
+ }
34
+ }
35
+
36
+ test('POST /api/clawhub/install materializes bundle files into the workspace skills directory', () => {
37
+ const output = runWithTempDataDir(`
38
+ const fs = await import('node:fs')
39
+ const path = await import('node:path')
40
+ const JSZip = (await import('jszip')).default
41
+ const storageMod = await import('./src/lib/server/storage')
42
+ const routeMod = await import('./src/app/api/clawhub/install/route')
43
+ const storage = storageMod.default || storageMod
44
+ const route = routeMod.default || routeMod
45
+
46
+ const archive = new JSZip()
47
+ archive.file('SKILL.md', \`---
48
+ name: test-hub-skill
49
+ description: A ClawHub test skill.
50
+ ---
51
+
52
+ # Test Hub Skill
53
+
54
+ Use this skill when the user asks for a ClawHub installation test.
55
+ \`)
56
+ archive.file('scripts/run.sh', '#!/bin/sh\\necho hi\\n')
57
+ archive.file('references/notes.md', '# Notes\\n')
58
+ const zipBuffer = await archive.generateAsync({ type: 'nodebuffer' })
59
+
60
+ globalThis.fetch = async () => new Response(zipBuffer, {
61
+ status: 200,
62
+ headers: { 'content-type': 'application/zip' },
63
+ })
64
+
65
+ const response = await route.POST(new Request('http://local/api/clawhub/install', {
66
+ method: 'POST',
67
+ headers: { 'content-type': 'application/json' },
68
+ body: JSON.stringify({
69
+ name: 'test-hub-skill',
70
+ description: 'A ClawHub test skill.',
71
+ url: 'https://clawhub.ai/skills/test-hub-skill',
72
+ author: 'ClawHub',
73
+ tags: ['test'],
74
+ }),
75
+ }))
76
+ const payload = await response.json()
77
+ const skillsDir = path.join(process.env.SWARMCLAW_HOME, 'skills', 'test-hub-skill')
78
+ const storedSkills = storage.loadSkills()
79
+ const stored = Object.values(storedSkills).find((skill) => skill.name === 'test-hub-skill')
80
+
81
+ console.log(JSON.stringify({
82
+ status: response.status,
83
+ installedName: payload?.name || null,
84
+ storedSkillId: stored?.id || null,
85
+ hasWorkspaceSkill: fs.existsSync(path.join(skillsDir, 'SKILL.md')),
86
+ hasWorkspaceScript: fs.existsSync(path.join(skillsDir, 'scripts', 'run.sh')),
87
+ hasWorkspaceReference: fs.existsSync(path.join(skillsDir, 'references', 'notes.md')),
88
+ }))
89
+ `)
90
+
91
+ assert.equal(output.status, 200)
92
+ assert.equal(output.installedName, 'test-hub-skill')
93
+ assert.notEqual(output.storedSkillId, null)
94
+ assert.equal(output.hasWorkspaceSkill, true)
95
+ assert.equal(output.hasWorkspaceScript, true)
96
+ assert.equal(output.hasWorkspaceReference, true)
97
+ })
@@ -1,9 +1,74 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
1
3
  import { NextResponse } from 'next/server'
2
4
  import { genId } from '@/lib/id'
3
5
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
- import { fetchSkillContent } from '@/lib/server/skills/clawhub-client'
6
+ import type { ClawHubSkillBundle } from '@/lib/server/skills/clawhub-client'
7
+ import { fetchClawHubSkillBundle, fetchSkillContent } from '@/lib/server/skills/clawhub-client'
8
+ import { clearDiscoveredSkillsCache, resolveWorkspaceSkillsDir } from '@/lib/server/skills/skill-discovery'
5
9
  import { normalizeSkillPayload } from '@/lib/server/skills/skills-normalize'
6
10
 
11
+ function sanitizeSkillDirName(value: string): string {
12
+ return value
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9._-]+/g, '-')
16
+ .replace(/^-+|-+$/g, '')
17
+ .slice(0, 80) || 'skill'
18
+ }
19
+
20
+ function normalizeBundlePath(filePath: string): string | null {
21
+ const normalized = path.posix.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, '')
22
+ if (!normalized || normalized === '.' || normalized.startsWith('/') || normalized.includes('\0')) return null
23
+ return normalized
24
+ }
25
+
26
+ function stripSharedTopLevelDir(paths: string[]): string[] {
27
+ const splitPaths = paths.map((filePath) => filePath.split('/').filter(Boolean))
28
+ const sharedRoot = splitPaths[0]?.[0]
29
+ if (!sharedRoot) return paths
30
+ const shouldStrip = splitPaths.every((parts) => parts.length > 1 && parts[0] === sharedRoot)
31
+ return shouldStrip
32
+ ? splitPaths.map((parts) => parts.slice(1).join('/'))
33
+ : paths
34
+ }
35
+
36
+ async function materializeClawHubBundle(url: string): Promise<string | null> {
37
+ const bundle = await fetchClawHubSkillBundle(url)
38
+ if (!bundle) return null
39
+ await writeClawHubBundleToWorkspace(bundle)
40
+ return bundle.content
41
+ }
42
+
43
+ async function writeClawHubBundleToWorkspace(bundle: ClawHubSkillBundle): Promise<void> {
44
+ const normalizedEntries = bundle.files
45
+ .map((file) => ({
46
+ file,
47
+ path: normalizeBundlePath(file.path),
48
+ }))
49
+ .filter((entry): entry is { file: ClawHubSkillBundle['files'][number], path: string } => Boolean(entry.path))
50
+
51
+ const workspaceSkillsDir = resolveWorkspaceSkillsDir()
52
+ const targetDir = path.join(workspaceSkillsDir, sanitizeSkillDirName(bundle.slug))
53
+ const normalizedPaths = stripSharedTopLevelDir(normalizedEntries.map((entry) => entry.path))
54
+
55
+ await fs.rm(targetDir, { recursive: true, force: true })
56
+ await fs.mkdir(targetDir, { recursive: true })
57
+
58
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
59
+ const relativePath = normalizedPaths[index]
60
+ if (!relativePath) continue
61
+ const destination = path.join(targetDir, relativePath)
62
+ if (!destination.startsWith(targetDir + path.sep) && destination !== targetDir) {
63
+ throw new Error(`Refusing to write bundle file outside the target directory: ${relativePath}`)
64
+ }
65
+ await fs.mkdir(path.dirname(destination), { recursive: true })
66
+ await fs.writeFile(destination, normalizedEntries[index].file.content)
67
+ }
68
+
69
+ clearDiscoveredSkillsCache()
70
+ }
71
+
7
72
  export async function POST(req: Request) {
8
73
  const body = await req.json()
9
74
  const { name, description, url, author, tags } = body
@@ -11,7 +76,7 @@ export async function POST(req: Request) {
11
76
 
12
77
  if (!content) {
13
78
  try {
14
- content = await fetchSkillContent(url)
79
+ content = await materializeClawHubBundle(url) || await fetchSkillContent(url)
15
80
  } catch (err: unknown) {
16
81
  return NextResponse.json(
17
82
  { error: err instanceof Error ? err.message : 'Failed to fetch skill content' },
@@ -30,8 +95,14 @@ export async function POST(req: Request) {
30
95
  })
31
96
 
32
97
  const skills = loadSkills()
33
- const id = genId()
98
+ const duplicate = Object.values(skills).find((skill) => {
99
+ const left = (skill.skillKey || skill.name || '').trim().toLowerCase()
100
+ const right = (normalized.skillKey || normalized.name || '').trim().toLowerCase()
101
+ return left && right && left === right
102
+ })
103
+ const id = duplicate?.id || genId()
34
104
  skills[id] = {
105
+ ...(duplicate || {}),
35
106
  id,
36
107
  name: normalized.name,
37
108
  filename: normalized.filename || `skill-${id}.md`,
@@ -52,10 +123,13 @@ export async function POST(req: Request) {
52
123
  skillRequirements: normalized.skillRequirements,
53
124
  detectedEnvVars: normalized.detectedEnvVars,
54
125
  security: normalized.security,
126
+ invocation: normalized.invocation,
127
+ commandDispatch: normalized.commandDispatch,
55
128
  frontmatter: normalized.frontmatter,
56
- createdAt: Date.now(),
129
+ createdAt: duplicate?.createdAt || Date.now(),
57
130
  updatedAt: Date.now(),
58
131
  }
59
132
  saveSkills(skills)
133
+ clearDiscoveredSkillsCache()
60
134
  return NextResponse.json(skills[id])
61
135
  }