@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -1,4 +1,5 @@
1
1
  import path from 'path'
2
+ import type { SkillInstallOption, SkillRequirements, SkillSecuritySummary } from '@/types'
2
3
 
3
4
  export type SkillSourceFormat = 'openclaw' | 'plain'
4
5
 
@@ -8,6 +9,19 @@ type NormalizeSkillInput = {
8
9
  filename?: unknown
9
10
  content?: unknown
10
11
  sourceUrl?: unknown
12
+ sourceFormat?: unknown
13
+ author?: unknown
14
+ tags?: unknown
15
+ version?: unknown
16
+ homepage?: unknown
17
+ primaryEnv?: unknown
18
+ skillKey?: unknown
19
+ always?: unknown
20
+ installOptions?: unknown
21
+ skillRequirements?: unknown
22
+ detectedEnvVars?: unknown
23
+ security?: unknown
24
+ frontmatter?: unknown
11
25
  }
12
26
 
13
27
  export type NormalizedSkill = {
@@ -17,6 +31,23 @@ export type NormalizedSkill = {
17
31
  content: string
18
32
  sourceUrl?: string
19
33
  sourceFormat: SkillSourceFormat
34
+ author?: string
35
+ tags?: string[]
36
+ version?: string
37
+ homepage?: string
38
+ primaryEnv?: string | null
39
+ skillKey?: string | null
40
+ always?: boolean
41
+ installOptions?: SkillInstallOption[]
42
+ skillRequirements?: SkillRequirements
43
+ detectedEnvVars?: string[]
44
+ security?: SkillSecuritySummary | null
45
+ frontmatter?: Record<string, unknown> | null
46
+ }
47
+
48
+ type ParsedFrontmatter = {
49
+ frontmatter: Record<string, unknown>
50
+ body: string
20
51
  }
21
52
 
22
53
  function asTrimmedString(value: unknown): string | null {
@@ -25,8 +56,30 @@ function asTrimmedString(value: unknown): string | null {
25
56
  return trimmed.length > 0 ? trimmed : null
26
57
  }
27
58
 
59
+ function asStringArray(value: unknown): string[] | undefined {
60
+ if (!Array.isArray(value)) return undefined
61
+ const items = value
62
+ .map((item) => asTrimmedString(item))
63
+ .filter((item): item is string => Boolean(item))
64
+ return items.length ? items : undefined
65
+ }
66
+
67
+ function asBoolean(value: unknown): boolean | undefined {
68
+ if (typeof value === 'boolean') return value
69
+ if (typeof value === 'string') {
70
+ if (value.trim().toLowerCase() === 'true') return true
71
+ if (value.trim().toLowerCase() === 'false') return false
72
+ }
73
+ return undefined
74
+ }
75
+
76
+ function asObject(value: unknown): Record<string, unknown> | null {
77
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
78
+ return value as Record<string, unknown>
79
+ }
80
+
28
81
  function stripQuotes(value: string): string {
29
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
82
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
30
83
  return value.slice(1, -1)
31
84
  }
32
85
  return value
@@ -47,26 +100,6 @@ function sanitizeFilename(input: string): string {
47
100
  return safe.toLowerCase().endsWith('.md') ? safe : `${safe}.md`
48
101
  }
49
102
 
50
- function parseFrontmatterBlock(content: string): { frontmatter: Record<string, string>; body: string } | null {
51
- const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
52
- if (!match) return null
53
- const rawFrontmatter = match[1]
54
- const body = match[2] || ''
55
-
56
- const frontmatter: Record<string, string> = {}
57
- for (const rawLine of rawFrontmatter.split(/\r?\n/)) {
58
- const line = rawLine.trim()
59
- if (!line || line.startsWith('#')) continue
60
- const idx = line.indexOf(':')
61
- if (idx === -1) continue
62
- const key = line.slice(0, idx).trim().toLowerCase()
63
- const value = stripQuotes(line.slice(idx + 1).trim())
64
- if (key) frontmatter[key] = value
65
- }
66
-
67
- return { frontmatter, body }
68
- }
69
-
70
103
  function deriveNameFromFilename(filename: string): string {
71
104
  return filename
72
105
  .replace(/\.md$/i, '')
@@ -89,15 +122,285 @@ function deriveFilenameFromUrl(url: string): string | null {
89
122
  }
90
123
  }
91
124
 
125
+ function parseInlineArray(value: string): unknown[] {
126
+ const inner = value.slice(1, -1).trim()
127
+ if (!inner) return []
128
+ return inner
129
+ .split(',')
130
+ .map((part) => parseScalar(part.trim()))
131
+ }
132
+
133
+ function parseScalar(value: string): unknown {
134
+ const trimmed = value.trim()
135
+ if (!trimmed) return ''
136
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith('\'') && trimmed.endsWith('\''))) {
137
+ return stripQuotes(trimmed)
138
+ }
139
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
140
+ return parseInlineArray(trimmed)
141
+ }
142
+ if (/^(true|false)$/i.test(trimmed)) return trimmed.toLowerCase() === 'true'
143
+ if (/^(null|~)$/i.test(trimmed)) return null
144
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed)
145
+ return trimmed
146
+ }
147
+
148
+ function nextNestedContainer(lines: string[], start: number, currentIndent: number): Record<string, unknown> | unknown[] {
149
+ for (let index = start + 1; index < lines.length; index += 1) {
150
+ const raw = lines[index]
151
+ const trimmed = raw.trim()
152
+ if (!trimmed || trimmed.startsWith('#')) continue
153
+ const indent = raw.match(/^\s*/)?.[0]?.length || 0
154
+ if (indent <= currentIndent) break
155
+ return trimmed.startsWith('- ') ? [] : {}
156
+ }
157
+ return {}
158
+ }
159
+
160
+ function parseFrontmatterData(rawFrontmatter: string): Record<string, unknown> {
161
+ const lines = rawFrontmatter.split(/\r?\n/)
162
+ const root: Record<string, unknown> = {}
163
+ const stack: Array<{ indent: number; value: Record<string, unknown> | unknown[] }> = [{ indent: -1, value: root }]
164
+
165
+ for (let index = 0; index < lines.length; index += 1) {
166
+ const rawLine = lines[index]
167
+ const trimmed = rawLine.trim()
168
+ if (!trimmed || trimmed.startsWith('#')) continue
169
+ const indent = rawLine.match(/^\s*/)?.[0]?.length || 0
170
+
171
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
172
+ stack.pop()
173
+ }
174
+
175
+ const parent = stack[stack.length - 1]?.value
176
+ if (!parent) continue
177
+
178
+ if (trimmed.startsWith('- ')) {
179
+ if (!Array.isArray(parent)) continue
180
+ const rest = trimmed.slice(2).trim()
181
+ if (!rest) {
182
+ const container = nextNestedContainer(lines, index, indent)
183
+ parent.push(container)
184
+ stack.push({ indent, value: container })
185
+ continue
186
+ }
187
+
188
+ const objectItemMatch = rest.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/)
189
+ if (objectItemMatch) {
190
+ const [, key, rawValue] = objectItemMatch
191
+ const objectItem: Record<string, unknown> = {}
192
+ if (rawValue.trim()) {
193
+ objectItem[key] = parseScalar(rawValue)
194
+ parent.push(objectItem)
195
+ stack.push({ indent, value: objectItem })
196
+ } else {
197
+ const container = nextNestedContainer(lines, index, indent)
198
+ objectItem[key] = container
199
+ parent.push(objectItem)
200
+ stack.push({ indent, value: objectItem })
201
+ stack.push({ indent: indent + 1, value: container })
202
+ }
203
+ continue
204
+ }
205
+
206
+ parent.push(parseScalar(rest))
207
+ continue
208
+ }
209
+
210
+ if (Array.isArray(parent)) continue
211
+
212
+ const keyMatch = trimmed.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/)
213
+ if (!keyMatch) continue
214
+ const [, key, rawValue] = keyMatch
215
+ if (rawValue.trim()) {
216
+ parent[key] = parseScalar(rawValue)
217
+ continue
218
+ }
219
+ const container = nextNestedContainer(lines, index, indent)
220
+ parent[key] = container
221
+ stack.push({ indent, value: container })
222
+ }
223
+
224
+ return root
225
+ }
226
+
227
+ function parseFrontmatterBlock(content: string): ParsedFrontmatter | null {
228
+ const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
229
+ if (!match) return null
230
+ const rawFrontmatter = match[1]
231
+ const body = match[2] || ''
232
+ return {
233
+ frontmatter: parseFrontmatterData(rawFrontmatter),
234
+ body,
235
+ }
236
+ }
237
+
238
+ function normalizeInstallOptions(value: unknown): SkillInstallOption[] | undefined {
239
+ if (!Array.isArray(value)) return undefined
240
+ const normalized: SkillInstallOption[] = value.flatMap((entry) => {
241
+ const row = asObject(entry)
242
+ if (!row) return []
243
+ const kind = asTrimmedString(row.kind)
244
+ const label = asTrimmedString(row.label)
245
+ || asTrimmedString(row.formula)
246
+ || asTrimmedString(row.package)
247
+ || asTrimmedString(row.url)
248
+ if (!kind || !label) return []
249
+ if (!['brew', 'node', 'go', 'uv', 'download'].includes(kind)) return []
250
+ return [{
251
+ kind: kind as SkillInstallOption['kind'],
252
+ label,
253
+ bins: asStringArray(row.bins),
254
+ } satisfies SkillInstallOption]
255
+ })
256
+ return normalized.length ? normalized : undefined
257
+ }
258
+
259
+ function normalizeRequirements(value: unknown): SkillRequirements | undefined {
260
+ const source = asObject(value)
261
+ if (!source) return undefined
262
+ const requires = asObject(source.requires) || source
263
+ const anyBins = Array.isArray(requires.anyBins)
264
+ ? requires.anyBins
265
+ .map((group) => asStringArray(group) || [])
266
+ .filter((group) => group.length > 0)
267
+ : undefined
268
+ const normalized: SkillRequirements = {
269
+ bins: asStringArray(requires.bins),
270
+ anyBins,
271
+ env: asStringArray(requires.env),
272
+ config: asStringArray(requires.config),
273
+ os: asStringArray(source.os ?? requires.os),
274
+ }
275
+ if (!normalized.bins && !normalized.anyBins && !normalized.env && !normalized.config && !normalized.os) {
276
+ return undefined
277
+ }
278
+ return normalized
279
+ }
280
+
281
+ function pickRuntimeMetadata(frontmatter: Record<string, unknown>): Record<string, unknown> | null {
282
+ const metadata = asObject(frontmatter.metadata)
283
+ if (metadata) {
284
+ const scoped = asObject(metadata.openclaw)
285
+ || asObject(metadata.clawdbot)
286
+ || asObject(metadata.clawdis)
287
+ if (scoped) return scoped
288
+ }
289
+ return asObject(frontmatter.openclaw)
290
+ || asObject(frontmatter.clawdbot)
291
+ || asObject(frontmatter.clawdis)
292
+ }
293
+
294
+ function uniqueStrings(values: string[]): string[] {
295
+ return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))
296
+ }
297
+
298
+ function extractDetectedEnvVars(rawContent: string): string[] {
299
+ const detected = new Set<string>()
300
+ const patterns = [
301
+ /process\.env\.([A-Z][A-Z0-9_]+)/g,
302
+ /\$\{([A-Z][A-Z0-9_]+)\}/g,
303
+ /\bexport\s+([A-Z][A-Z0-9_]+)\b/g,
304
+ ]
305
+ for (const pattern of patterns) {
306
+ let match: RegExpExecArray | null
307
+ while ((match = pattern.exec(rawContent)) !== null) {
308
+ detected.add(match[1])
309
+ }
310
+ }
311
+ return [...detected].sort()
312
+ }
313
+
314
+ function extractInstallCommands(rawContent: string): string[] {
315
+ const commands: string[] = []
316
+ for (const line of rawContent.split(/\r?\n/)) {
317
+ const trimmed = line.trim()
318
+ if (!trimmed) continue
319
+ if (
320
+ /(brew install|npm install|pnpm add|yarn add|go install|uv tool install|curl .*\|\s*(?:bash|sh)|wget .*\|\s*(?:bash|sh))/i.test(trimmed)
321
+ ) {
322
+ commands.push(trimmed)
323
+ }
324
+ }
325
+ return uniqueStrings(commands).slice(0, 8)
326
+ }
327
+
328
+ function buildSkillSecuritySummary(params: {
329
+ rawContent: string
330
+ requirements?: SkillRequirements
331
+ installOptions?: SkillInstallOption[]
332
+ primaryEnv?: string | null
333
+ }): SkillSecuritySummary | null {
334
+ const detectedEnvVars = extractDetectedEnvVars(params.rawContent)
335
+ const declaredEnv = new Set<string>([
336
+ ...(params.requirements?.env || []),
337
+ ...(params.primaryEnv ? [params.primaryEnv] : []),
338
+ ])
339
+ const missingDeclarations = detectedEnvVars.filter((name) => !declaredEnv.has(name))
340
+ const installCommands = extractInstallCommands(params.rawContent)
341
+ const notes: string[] = []
342
+
343
+ if (missingDeclarations.length) {
344
+ notes.push(`Detected env vars missing from frontmatter: ${missingDeclarations.join(', ')}`)
345
+ }
346
+ if (installCommands.length) {
347
+ notes.push('Skill content includes install instructions or executable bootstrap commands.')
348
+ }
349
+ if ((params.installOptions?.length || 0) > 0) {
350
+ notes.push('Skill declares install options that should be reviewed before enabling.')
351
+ }
352
+ if (/(curl .*\|\s*(?:bash|sh)|wget .*\|\s*(?:bash|sh)|sudo\s+)/i.test(params.rawContent)) {
353
+ notes.push('Skill content includes high-risk shell patterns.')
354
+ }
355
+
356
+ if (!notes.length && !detectedEnvVars.length && !installCommands.length) return null
357
+
358
+ const level: SkillSecuritySummary['level'] =
359
+ notes.some((note) => /high-risk|missing from frontmatter/i.test(note))
360
+ ? 'high'
361
+ : installCommands.length || detectedEnvVars.length
362
+ ? 'medium'
363
+ : 'low'
364
+
365
+ return {
366
+ level,
367
+ notes,
368
+ detectedEnvVars,
369
+ missingDeclarations,
370
+ installCommands,
371
+ }
372
+ }
373
+
92
374
  export function normalizeSkillPayload(input: NormalizeSkillInput): NormalizedSkill {
93
375
  const rawContent = typeof input.content === 'string' ? input.content : ''
94
376
  const parsed = parseFrontmatterBlock(rawContent)
377
+ const preservedFrontmatter = asObject(input.frontmatter)
378
+ const frontmatter = parsed?.frontmatter || preservedFrontmatter || null
379
+ const runtimeMeta = frontmatter ? pickRuntimeMetadata(frontmatter) : null
95
380
 
96
- const frontmatterName = asTrimmedString(parsed?.frontmatter?.name)
97
- const frontmatterDescription = asTrimmedString(parsed?.frontmatter?.description)
381
+ const frontmatterName = asTrimmedString(frontmatter?.name)
382
+ const frontmatterDescription = asTrimmedString(frontmatter?.description)
383
+ const frontmatterAuthor = asTrimmedString(frontmatter?.author)
384
+ const frontmatterTags = asStringArray(frontmatter?.tags)
385
+ const version = asTrimmedString(frontmatter?.version)
386
+ || asTrimmedString(runtimeMeta?.version)
387
+ || asTrimmedString(input.version)
388
+ const homepage = asTrimmedString(runtimeMeta?.homepage)
389
+ || asTrimmedString(frontmatter?.homepage)
390
+ || asTrimmedString(input.homepage)
391
+ const primaryEnv = asTrimmedString(runtimeMeta?.primaryEnv)
392
+ || asTrimmedString(input.primaryEnv)
393
+ || null
394
+ const skillKey = asTrimmedString(runtimeMeta?.skillKey)
395
+ || asTrimmedString(input.skillKey)
396
+ || null
397
+ const always = asBoolean(runtimeMeta?.always) ?? asBoolean(input.always)
398
+ const installOptions = normalizeInstallOptions(runtimeMeta?.install)
399
+ || normalizeInstallOptions(input.installOptions)
400
+ const skillRequirements = normalizeRequirements(runtimeMeta)
401
+ || normalizeRequirements(input.skillRequirements)
98
402
 
99
403
  const sourceUrl = asTrimmedString(input.sourceUrl) || undefined
100
-
101
404
  const initialFilename = asTrimmedString(input.filename)
102
405
  || (sourceUrl ? deriveFilenameFromUrl(sourceUrl) : null)
103
406
  || (frontmatterName ? `${slugify(frontmatterName)}.md` : null)
@@ -112,10 +415,41 @@ export function normalizeSkillPayload(input: NormalizeSkillInput): NormalizedSki
112
415
  || frontmatterDescription
113
416
  || ''
114
417
 
115
- // For OpenClaw SKILL.md, keep only body instructions when frontmatter exists.
418
+ const author = asTrimmedString(input.author)
419
+ || frontmatterAuthor
420
+ || undefined
421
+
422
+ const tags = asStringArray(input.tags)
423
+ || frontmatterTags
424
+ || undefined
425
+
116
426
  const normalizedContent = parsed ? parsed.body.trimStart() : rawContent
427
+ const detectedEnvVars = extractDetectedEnvVars(rawContent)
428
+ const preservedDetectedEnvVars = asStringArray(input.detectedEnvVars)
429
+ const generatedSecurity = buildSkillSecuritySummary({
430
+ rawContent,
431
+ requirements: skillRequirements,
432
+ installOptions,
433
+ primaryEnv,
434
+ })
435
+ const securityRecord = asObject(input.security)
436
+ const security = generatedSecurity || (securityRecord
437
+ ? {
438
+ level: securityRecord.level === 'high' || securityRecord.level === 'medium' ? securityRecord.level : 'low',
439
+ notes: asStringArray(securityRecord.notes) || [],
440
+ detectedEnvVars: asStringArray(securityRecord.detectedEnvVars),
441
+ missingDeclarations: asStringArray(securityRecord.missingDeclarations),
442
+ installCommands: asStringArray(securityRecord.installCommands),
443
+ } satisfies SkillSecuritySummary
444
+ : null)
117
445
 
118
- const sourceFormat: SkillSourceFormat = parsed && (frontmatterName !== null || frontmatterDescription !== null || parsed.frontmatter.metadata)
446
+ const sourceFormat: SkillSourceFormat = (parsed && (
447
+ frontmatterName !== null
448
+ || frontmatterDescription !== null
449
+ || runtimeMeta !== null
450
+ ))
451
+ || input.sourceFormat === 'openclaw'
452
+ || preservedFrontmatter !== null
119
453
  ? 'openclaw'
120
454
  : 'plain'
121
455
 
@@ -126,5 +460,17 @@ export function normalizeSkillPayload(input: NormalizeSkillInput): NormalizedSki
126
460
  content: normalizedContent,
127
461
  sourceUrl,
128
462
  sourceFormat,
463
+ author,
464
+ tags,
465
+ version: version || undefined,
466
+ homepage: homepage || undefined,
467
+ primaryEnv,
468
+ skillKey,
469
+ always,
470
+ installOptions,
471
+ skillRequirements,
472
+ detectedEnvVars: detectedEnvVars.length ? detectedEnvVars : preservedDetectedEnvVars,
473
+ security,
474
+ frontmatter,
129
475
  }
130
476
  }