@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,142 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { normalizePlatformActionArgs } from './platform'
4
+
5
+ describe('normalizePlatformActionArgs', () => {
6
+ it('packs top-level create fields into data', () => {
7
+ const out = normalizePlatformActionArgs({
8
+ resource: 'tasks',
9
+ action: 'create',
10
+ title: 'Write docs',
11
+ agentId: 'default',
12
+ })
13
+
14
+ assert.equal(out.resource, 'tasks')
15
+ assert.equal(out.action, 'create')
16
+ assert.deepEqual(JSON.parse(String(out.data)), {
17
+ title: 'Write docs',
18
+ agentId: 'default',
19
+ })
20
+ })
21
+
22
+ it('merges object data with top-level overrides', () => {
23
+ const out = normalizePlatformActionArgs({
24
+ resource: 'tasks',
25
+ action: 'create',
26
+ data: { title: 'Old title', agentId: 'coder' },
27
+ title: 'New title',
28
+ })
29
+
30
+ assert.deepEqual(JSON.parse(String(out.data)), {
31
+ title: 'New title',
32
+ agentId: 'coder',
33
+ })
34
+ })
35
+
36
+ it('normalizes legacy resources envelope with parameters payload', () => {
37
+ const out = normalizePlatformActionArgs({
38
+ input: JSON.stringify({
39
+ resources: [
40
+ {
41
+ resource: 'tasks',
42
+ action: 'create',
43
+ parameters: {
44
+ title: 'Legacy task',
45
+ assigned_agent: 'default',
46
+ },
47
+ },
48
+ ],
49
+ }),
50
+ })
51
+
52
+ assert.equal(out.resource, 'tasks')
53
+ assert.equal(out.action, 'create')
54
+ assert.deepEqual(JSON.parse(String(out.data)), {
55
+ title: 'Legacy task',
56
+ assigned_agent: 'default',
57
+ })
58
+ })
59
+
60
+ it('normalizes singular resource names and resource payload objects', () => {
61
+ const out = normalizePlatformActionArgs({
62
+ input: JSON.stringify({
63
+ resource: 'task',
64
+ action: 'create',
65
+ task: {
66
+ title: 'Legacy singular task',
67
+ assigned_to: 'default',
68
+ },
69
+ }),
70
+ })
71
+
72
+ assert.equal(out.resource, 'tasks')
73
+ assert.equal(out.action, 'create')
74
+ assert.deepEqual(JSON.parse(String(out.data)), {
75
+ title: 'Legacy singular task',
76
+ assigned_to: 'default',
77
+ })
78
+ })
79
+
80
+ it('normalizes legacy backlog task resource names to tasks', () => {
81
+ const out = normalizePlatformActionArgs({
82
+ input: JSON.stringify({
83
+ resource: 'backlog_task',
84
+ action: 'create',
85
+ backlog_task: {
86
+ title: 'Legacy backlog task',
87
+ description: 'Keep the intended task payload',
88
+ },
89
+ }),
90
+ })
91
+
92
+ assert.equal(out.resource, 'tasks')
93
+ assert.equal(out.action, 'create')
94
+ assert.deepEqual(JSON.parse(String(out.data)), {
95
+ title: 'Legacy backlog task',
96
+ description: 'Keep the intended task payload',
97
+ })
98
+ })
99
+
100
+ it('normalizes resources entries that use type instead of resource', () => {
101
+ const out = normalizePlatformActionArgs({
102
+ input: JSON.stringify({
103
+ action: 'create',
104
+ resources: [
105
+ {
106
+ type: 'task',
107
+ parameters: {
108
+ title: 'Typed task resource',
109
+ description: 'Created through a typed resources envelope',
110
+ },
111
+ },
112
+ ],
113
+ }),
114
+ })
115
+
116
+ assert.equal(out.resource, 'tasks')
117
+ assert.equal(out.action, 'create')
118
+ assert.deepEqual(JSON.parse(String(out.data)), {
119
+ title: 'Typed task resource',
120
+ description: 'Created through a typed resources envelope',
121
+ })
122
+ })
123
+
124
+ it('infers schedules resource from create_schedule style actions', () => {
125
+ const out = normalizePlatformActionArgs({
126
+ input: JSON.stringify({
127
+ action: 'create_schedule',
128
+ data: {
129
+ name: 'Surgery check-in',
130
+ scheduleType: 'once',
131
+ },
132
+ }),
133
+ })
134
+
135
+ assert.equal(out.resource, 'schedules')
136
+ assert.equal(out.action, 'create')
137
+ assert.deepEqual(JSON.parse(String(out.data)), {
138
+ name: 'Surgery check-in',
139
+ scheduleType: 'once',
140
+ })
141
+ })
142
+ })
@@ -6,12 +6,150 @@ import type { Plugin, PluginHooks } from '@/types'
6
6
  import { getPluginManager } from '../plugins'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
8
 
9
+ function parsePlatformData(value: unknown): Record<string, unknown> | null {
10
+ if (!value) return null
11
+ if (typeof value === 'object' && !Array.isArray(value)) {
12
+ return value as Record<string, unknown>
13
+ }
14
+ if (typeof value !== 'string') return null
15
+ const trimmed = value.trim()
16
+ if (!trimmed) return null
17
+ try {
18
+ const parsed = JSON.parse(trimmed)
19
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
20
+ return parsed as Record<string, unknown>
21
+ }
22
+ } catch {
23
+ // Preserve non-JSON data strings as-is in the caller.
24
+ }
25
+ return null
26
+ }
27
+
28
+ function firstPlatformResource(value: unknown): Record<string, unknown> | null {
29
+ if (!Array.isArray(value)) return null
30
+ const first = value.find((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))
31
+ return first && typeof first === 'object' && !Array.isArray(first)
32
+ ? first as Record<string, unknown>
33
+ : null
34
+ }
35
+
36
+ function normalizePlatformResourceName(value: unknown): string | undefined {
37
+ if (typeof value !== 'string') return undefined
38
+ const normalized = value.trim().toLowerCase()
39
+ if (!normalized) return undefined
40
+ const singularMap: Record<string, string> = {
41
+ agent: 'agents',
42
+ task: 'tasks',
43
+ backlog_task: 'tasks',
44
+ 'backlog-task': 'tasks',
45
+ backlogtask: 'tasks',
46
+ task_backlog: 'tasks',
47
+ 'task-backlog': 'tasks',
48
+ work_item: 'tasks',
49
+ 'work-item': 'tasks',
50
+ schedule: 'schedules',
51
+ skill: 'skills',
52
+ document: 'documents',
53
+ secret: 'secrets',
54
+ connector: 'connectors',
55
+ session: 'sessions',
56
+ }
57
+ return singularMap[normalized] || normalized
58
+ }
59
+
60
+ function inferPlatformResourceFromAction(value: unknown): { resource?: string; action?: string } {
61
+ if (typeof value !== 'string') return {}
62
+ const normalized = value.trim().toLowerCase().replace(/-/g, '_')
63
+ if (!normalized) return {}
64
+ const match = normalized.match(/^(list|get|create|update|delete)_([a-z_]+)$/)
65
+ if (!match) return {}
66
+ const [, action, rawResource] = match
67
+ const resource = normalizePlatformResourceName(rawResource)
68
+ if (!resource) return {}
69
+ return { resource, action }
70
+ }
71
+
72
+ function extractPlatformFields(value: Record<string, unknown>): Record<string, unknown> {
73
+ const fields: Record<string, unknown> = {}
74
+ for (const [key, fieldValue] of Object.entries(value)) {
75
+ if (fieldValue === undefined || fieldValue === null) continue
76
+ if (['input', 'args', 'arguments', 'payload', 'resources', 'parameters', 'resource', 'type', 'action', 'id'].includes(key)) continue
77
+ fields[key] = fieldValue
78
+ }
79
+ return fields
80
+ }
81
+
82
+ export function normalizePlatformActionArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
83
+ const normalized = normalizeToolInputArgs(rawArgs)
84
+ const resourceEntry = firstPlatformResource(normalized.resources)
85
+ const { resource, action, id, data, ...rest } = normalized
86
+ const payload: Record<string, unknown> = {}
87
+ const resourceValue = resource ?? resourceEntry?.resource ?? resourceEntry?.type
88
+ const rawResourceName = typeof resourceValue === 'string'
89
+ ? String(resourceValue).trim()
90
+ : undefined
91
+
92
+ const rawAction = action ?? resourceEntry?.action
93
+ const inferredFromAction = resourceValue === undefined
94
+ ? inferPlatformResourceFromAction(rawAction)
95
+ : {}
96
+ const effectiveResource = normalizePlatformResourceName(resourceValue) ?? inferredFromAction.resource
97
+ const effectiveAction = inferredFromAction.action && resourceValue === undefined
98
+ ? inferredFromAction.action
99
+ : rawAction
100
+ const effectiveId = id ?? resourceEntry?.id
101
+
102
+ if (effectiveResource !== undefined) payload.resource = effectiveResource
103
+ if (effectiveAction !== undefined) payload.action = effectiveAction
104
+ if (effectiveId !== undefined) payload.id = effectiveId
105
+
106
+ const directFields = extractPlatformFields(rest)
107
+ const resourcePayloadCandidates = effectiveResource
108
+ ? uniqueStrings([
109
+ rawResourceName,
110
+ effectiveResource,
111
+ effectiveResource.replace(/s$/, ''),
112
+ ])
113
+ : []
114
+ const directResourcePayload = resourcePayloadCandidates
115
+ .map((candidate) => parsePlatformData(normalized[candidate]))
116
+ .find(Boolean)
117
+ || null
118
+ if (effectiveResource) {
119
+ for (const candidate of resourcePayloadCandidates) delete directFields[candidate]
120
+ }
121
+ const parameterFields = {
122
+ ...(parsePlatformData(resourceEntry?.parameters) || {}),
123
+ ...(parsePlatformData(resourceEntry?.params) || {}),
124
+ ...(parsePlatformData(normalized.parameters) || {}),
125
+ ...(directResourcePayload || {}),
126
+ }
127
+ const parsedData = parsePlatformData(data)
128
+ const mergedData = {
129
+ ...(parsedData || {}),
130
+ ...parameterFields,
131
+ ...directFields,
132
+ }
133
+
134
+ if (Object.keys(mergedData).length > 0) {
135
+ payload.data = JSON.stringify(mergedData)
136
+ } else if (typeof data === 'string' && data.trim()) {
137
+ payload.data = data
138
+ }
139
+
140
+ return payload
141
+ }
142
+
143
+ function uniqueStrings(values: Array<string | undefined>): string[] {
144
+ return [...new Set(values.filter((value): value is string => Boolean(value)))]
145
+ }
146
+
9
147
  /**
10
148
  * Unified Platform Execution Logic
11
149
  */
12
150
  async function executePlatformAction(args: any, bctx: any) {
13
- const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
14
- const { resource, action, id, data, ...rest } = normalized
151
+ const normalized = normalizePlatformActionArgs((args ?? {}) as Record<string, unknown>)
152
+ const { resource, action, id, data } = normalized
15
153
 
16
154
  // We reuse the existing CRUD tool logic but expose it via a single tool
17
155
  const crudTools = buildCrudTools({
@@ -36,7 +174,7 @@ async function executePlatformAction(args: any, bctx: any) {
36
174
  }
37
175
 
38
176
  // Forward to the specific CRUD tool implementation
39
- return targetTool.invoke({ action, id, data, ...rest })
177
+ return targetTool.invoke({ action, id, data })
40
178
  }
41
179
 
42
180
  /**
@@ -52,7 +190,7 @@ const PlatformPlugin: Plugin = {
52
190
  tools: [
53
191
  {
54
192
  name: 'manage_platform',
55
- description: 'Unified tool for managing all SwarmClaw resources.',
193
+ description: 'Unified tool for managing all SwarmClaw resources. For create/update, pass resource + action, then either put fields inside data, pass them as top-level fields, or use a single resources[0].parameters envelope.',
56
194
  parameters: {
57
195
  type: 'object',
58
196
  properties: {
@@ -27,6 +27,8 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
27
27
  const action = normalized.action as string | undefined
28
28
  const filename = (normalized.filename ?? normalized.fileName) as string | undefined
29
29
  const code = (normalized.code ?? normalized.content) as string | undefined
30
+ const packageJson = normalized.packageJson ?? normalized.package_json ?? normalized.manifest
31
+ const packageManager = typeof normalized.packageManager === 'string' ? normalized.packageManager : undefined
30
32
  const approved = normalized.approved as boolean | undefined
31
33
 
32
34
  try {
@@ -40,15 +42,23 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
40
42
 
41
43
  // REQUIRE USER APPROVAL
42
44
  if (approved !== true) {
43
- const { requestApproval } = await import('../approvals')
44
- requestApproval({
45
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
46
+ const approval = await requestApprovalMaybeAutoApprove({
45
47
  category: 'plugin_scaffold',
46
48
  title: `Scaffold Plugin: ${filename}`,
47
49
  description: `Create new plugin file with ${code.length} chars of code.`,
48
- data: { filename, code, createdByAgentId: pctx.agentId || null },
50
+ data: { filename, code, packageJson, packageManager, createdByAgentId: pctx.agentId || null },
49
51
  agentId: pctx.agentId,
50
52
  sessionId: pctx.sessionId,
51
53
  })
54
+ if (approval.status === 'approved') {
55
+ return JSON.stringify({
56
+ type: 'plugin_scaffold_request',
57
+ filename,
58
+ autoApproved: true,
59
+ message: `Plugin "${filename}" was auto-approved and scaffolded. It is now available in this chat.`,
60
+ })
61
+ }
52
62
  return JSON.stringify({
53
63
  type: 'plugin_scaffold_request',
54
64
  filename,
@@ -56,12 +66,13 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
56
66
  })
57
67
  }
58
68
 
59
- const filePath = path.join(PLUGINS_DIR, filename)
60
- fs.writeFileSync(filePath, code, 'utf8')
61
-
62
- // Reload the plugin manager so the new plugin is discovered
63
69
  const manager = getPluginManager()
64
- manager.reload()
70
+ await manager.savePluginSource(filename, code, {
71
+ packageJson,
72
+ packageManager,
73
+ installDependencies: packageJson !== undefined,
74
+ })
75
+ const filePath = path.join(PLUGINS_DIR, filename)
65
76
 
66
77
  // Auto-enable the plugin for the agent that created it
67
78
  if (pctx.agentId && pctx.sessionId) {
@@ -82,10 +93,53 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
82
93
  return `Plugin saved to ${filePath} and PluginManager reloaded. It is now enabled for this chat.`
83
94
  }
84
95
 
96
+ if (action === 'install_dependencies') {
97
+ if (!filename) return 'Error: filename is required for install_dependencies.'
98
+
99
+ if (approved !== true) {
100
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
101
+ const approval = await requestApprovalMaybeAutoApprove({
102
+ category: 'plugin_install',
103
+ title: `Install Plugin Dependencies: ${filename}`,
104
+ description: `Install package dependencies for plugin ${filename}${packageManager ? ` using ${packageManager}` : ''}.`,
105
+ data: { filename, packageJson, packageManager, createdByAgentId: pctx.agentId || null },
106
+ agentId: pctx.agentId,
107
+ sessionId: pctx.sessionId,
108
+ })
109
+ if (approval.status === 'approved') {
110
+ return JSON.stringify({
111
+ type: 'plugin_install_request',
112
+ filename,
113
+ autoApproved: true,
114
+ message: `Dependencies for "${filename}" were auto-approved and are being installed.`,
115
+ })
116
+ }
117
+ return JSON.stringify({
118
+ type: 'plugin_install_request',
119
+ filename,
120
+ message: `I've requested approval to install dependencies for "${filename}". Once approved, the plugin manager will run the selected package manager automatically.`,
121
+ })
122
+ }
123
+
124
+ const manager = getPluginManager()
125
+ if (packageJson !== undefined) {
126
+ const source = manager.readPluginSource(filename)
127
+ await manager.savePluginSource(filename, source, {
128
+ packageJson,
129
+ packageManager,
130
+ installDependencies: false,
131
+ })
132
+ }
133
+ const result = await manager.installPluginDependencies(filename, {
134
+ packageManager: packageManager as import('@/types').PluginPackageManager | undefined,
135
+ })
136
+ return `Dependencies installed for ${filename} using ${result.packageManager || packageManager || 'npm'}.`
137
+ }
138
+
85
139
  if (action === 'get_spec') {
86
140
  return `
87
141
  SwarmClaw Plugin Specification:
88
- A plugin is a CommonJS module (.js) that must be DUAL-COMPATIBLE with both SwarmClaw and OpenClaw platforms.
142
+ A plugin is a JavaScript module (.js or .mjs) that can be dual-compatible with both SwarmClaw and OpenClaw platforms.
89
143
 
90
144
  \`\`\`js
91
145
  module.exports = {
@@ -100,10 +154,11 @@ module.exports = {
100
154
  hooks: {
101
155
  beforeAgentStart: async ({ session, message }) => {},
102
156
  afterAgentComplete: async ({ session, response }) => {},
103
- beforeToolExec: async ({ session, toolName, args }) => {},
104
- afterToolExec: async ({ session, toolName, result }) => {},
157
+ beforeToolExec: async ({ toolName, input }) => input,
158
+ afterToolExec: async ({ session, toolName, input, output }) => {},
105
159
  transformInboundMessage: async ({ session, text }) => { return text; },
106
160
  transformOutboundMessage: async ({ session, text }) => { return text; },
161
+ afterChatTurn: async ({ session, message, response, source, internal }) => {},
107
162
  },
108
163
 
109
164
  tools: [
@@ -140,27 +195,31 @@ module.exports = {
140
195
  \`\`\`
141
196
 
142
197
  Key rules:
143
- - Export BOTH SwarmClaw hooks/tools AND a register(api) method for cross-platform compatibility
144
- - SwarmClaw checks for hooks/tools first; OpenClaw checks for register()
198
+ - Export SwarmClaw hooks/tools. Add register(api) too if you want OpenClaw compatibility.
199
+ - SwarmClaw checks hooks/tools first; OpenClaw checks register()
145
200
  - Tools must have name, description, parameters (JSON Schema), and execute function
146
201
  - Hooks are optional — only include the ones you need
202
+ - If your plugin needs npm/pnpm/yarn/bun packages, include a packageJson object during scaffold or call install_dependencies later.
203
+ - Dependency installs are run by the plugin manager inside a per-plugin workspace using the selected package manager with scripts disabled.
204
+ - Plugin settings are declared through ui.settingsFields and stored per plugin ID
147
205
  - Keep plugins focused: one clear purpose per plugin
148
206
  `
149
207
  }
150
208
 
151
209
  if (action === 'read') {
152
210
  if (!filename) return 'Error: filename required.'
153
- const filePath = path.join(PLUGINS_DIR, filename)
154
- if (!fs.existsSync(filePath)) return `File not found: ${filename}`
155
- return fs.readFileSync(filePath, 'utf8')
211
+ return getPluginManager().readPluginSource(filename)
156
212
  }
157
213
 
158
214
  if (action === 'edit') {
159
215
  if (!filename || !code) return 'Error: filename and code are required for edit.'
160
- const filePath = path.join(PLUGINS_DIR, filename)
161
- if (!fs.existsSync(filePath)) return `File not found: ${filename}. Use scaffold to create new plugins.`
162
- fs.writeFileSync(filePath, code, 'utf8')
163
- getPluginManager().reload()
216
+ const manager = getPluginManager()
217
+ try {
218
+ manager.readPluginSource(filename)
219
+ } catch {
220
+ return `File not found: ${filename}. Use scaffold to create new plugins.`
221
+ }
222
+ await manager.savePluginSource(filename, code)
164
223
  return `Updated ${filename} and reloaded plugin manager.`
165
224
  }
166
225
 
@@ -175,7 +234,7 @@ Key rules:
175
234
  return `File not found: ${filename}`
176
235
  }
177
236
 
178
- return `Unknown action "${action}". Valid actions: get_spec, scaffold, read, edit, delete`
237
+ return `Unknown action "${action}". Valid actions: get_spec, scaffold, read, edit, delete, install_dependencies`
179
238
  } catch (err: unknown) {
180
239
  return `Error: ${err instanceof Error ? err.message : String(err)}`
181
240
  }
@@ -195,9 +254,11 @@ const PluginCreatorPlugin: Plugin = {
195
254
  parameters: {
196
255
  type: 'object',
197
256
  properties: {
198
- action: { type: 'string', enum: ['get_spec', 'scaffold', 'read', 'edit', 'delete'], description: 'get_spec: learn format. scaffold: create (needs approval). read: view code. edit: update existing. delete: remove.' },
257
+ action: { type: 'string', enum: ['get_spec', 'scaffold', 'read', 'edit', 'delete', 'install_dependencies'], description: 'get_spec: learn format. scaffold: create (needs approval). read: view code. edit: update existing. delete: remove. install_dependencies: write/read package.json and install runtime deps.' },
199
258
  filename: { type: 'string', description: 'Plugin filename, e.g. my-plugin.js. Required for scaffold and delete.' },
200
259
  code: { type: 'string', description: 'The raw JavaScript code for the plugin. Required for scaffold.' },
260
+ packageJson: { type: 'object', description: 'Optional package.json object for dependency-aware plugins. Use with scaffold or install_dependencies.' },
261
+ packageManager: { type: 'string', enum: ['npm', 'pnpm', 'yarn', 'bun'], description: 'Optional package manager to use for dependency installs.' },
201
262
  approved: { type: 'boolean', description: 'Internal flag — do NOT set this. The approval system handles it automatically.' }
202
263
  },
203
264
  required: ['action']
@@ -228,9 +289,11 @@ export function buildPluginCreatorTools(bctx: ToolBuildContext): StructuredToolI
228
289
  name: 'plugin_creator_tool',
229
290
  description: PluginCreatorPlugin.tools![0].description,
230
291
  schema: z.object({
231
- action: z.enum(['get_spec', 'scaffold', 'read', 'edit', 'delete']),
292
+ action: z.enum(['get_spec', 'scaffold', 'read', 'edit', 'delete', 'install_dependencies']),
232
293
  filename: z.string().optional(),
233
294
  code: z.string().optional(),
295
+ packageJson: z.record(z.string(), z.any()).optional(),
296
+ packageManager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
234
297
  approved: z.boolean().optional()
235
298
  })
236
299
  }