@swarmclawai/swarmclaw 0.7.1 → 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 (237) hide show
  1. package/README.md +155 -150
  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 +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  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/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -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) {
@@ -70,9 +81,9 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
70
81
  const sessions = loadSessions()
71
82
  const session = sessions[pctx.sessionId!]
72
83
  if (session) {
73
- const currentTools = session.tools || []
84
+ const currentTools = session.plugins || []
74
85
  if (!currentTools.includes(filename)) {
75
- session.tools = [...currentTools, filename]
86
+ session.plugins = [...currentTools, filename]
76
87
  saveSessions(sessions)
77
88
  }
78
89
  }
@@ -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']
@@ -220,7 +281,7 @@ getPluginManager().registerBuiltin('plugin_creator', PluginCreatorPlugin)
220
281
  * Legacy Bridge
221
282
  */
222
283
  export function buildPluginCreatorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
223
- if (!bctx.hasTool('plugin_creator')) return []
284
+ if (!bctx.hasPlugin('plugin_creator')) return []
224
285
  return [
225
286
  tool(
226
287
  async (args) => executePluginCreatorAction(args, bctx),
@@ -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
  }
@@ -0,0 +1,257 @@
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 { after, before, describe, it } from 'node:test'
6
+ import type { Session } from '@/types'
7
+
8
+ const originalEnv = {
9
+ DATA_DIR: process.env.DATA_DIR,
10
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
11
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
12
+ }
13
+
14
+ let tempDir = ''
15
+ let workspaceDir = ''
16
+ let buildDocumentTools: typeof import('./document').buildDocumentTools
17
+ let buildExtractTools: typeof import('./extract').buildExtractTools
18
+ let buildTableTools: typeof import('./table').buildTableTools
19
+ let buildMailboxTools: typeof import('./mailbox').buildMailboxTools
20
+ let buildHumanLoopTools: typeof import('./human-loop').buildHumanLoopTools
21
+ let buildCrawlTools: typeof import('./crawl').buildCrawlTools
22
+ let sessionMailbox: typeof import('../session-mailbox')
23
+ let watchJobs: typeof import('../watch-jobs')
24
+ let storage: typeof import('../storage')
25
+
26
+ function makeSession(overrides?: Partial<Session>): Session {
27
+ return {
28
+ id: 'session_1',
29
+ name: 'Test Session',
30
+ cwd: workspaceDir,
31
+ user: 'tester',
32
+ provider: 'ollama',
33
+ model: 'qwen3.5',
34
+ apiEndpoint: 'http://localhost:11434',
35
+ claudeSessionId: null,
36
+ messages: [],
37
+ createdAt: Date.now(),
38
+ lastActiveAt: Date.now(),
39
+ plugins: [],
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ function makeBuildContext(overrides?: {
45
+ cwd?: string
46
+ session?: Session
47
+ }) {
48
+ const session = overrides?.session || makeSession()
49
+ return {
50
+ cwd: overrides?.cwd || workspaceDir,
51
+ ctx: {
52
+ sessionId: session.id,
53
+ agentId: session.agentId || 'agent_1',
54
+ },
55
+ hasPlugin: () => true,
56
+ hasTool: () => true,
57
+ cleanupFns: [],
58
+ commandTimeoutMs: 5000,
59
+ claudeTimeoutMs: 5000,
60
+ cliProcessTimeoutMs: 5000,
61
+ persistDelegateResumeId: () => {},
62
+ readStoredDelegateResumeId: () => null,
63
+ resolveCurrentSession: () => session,
64
+ activePlugins: [],
65
+ }
66
+ }
67
+
68
+ before(async () => {
69
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-primitive-tools-'))
70
+ workspaceDir = path.join(tempDir, 'workspace')
71
+ fs.mkdirSync(workspaceDir, { recursive: true })
72
+ process.env.DATA_DIR = path.join(tempDir, 'data')
73
+ process.env.WORKSPACE_DIR = workspaceDir
74
+ process.env.SWARMCLAW_BUILD_MODE = '1'
75
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
76
+
77
+ ;({ buildDocumentTools } = await import('./document'))
78
+ ;({ buildExtractTools } = await import('./extract'))
79
+ ;({ buildTableTools } = await import('./table'))
80
+ ;({ buildMailboxTools } = await import('./mailbox'))
81
+ ;({ buildHumanLoopTools } = await import('./human-loop'))
82
+ ;({ buildCrawlTools } = await import('./crawl'))
83
+ sessionMailbox = await import('../session-mailbox')
84
+ watchJobs = await import('../watch-jobs')
85
+ storage = await import('../storage')
86
+ })
87
+
88
+ after(() => {
89
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
90
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
91
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
92
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
93
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
94
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
95
+ fs.rmSync(tempDir, { recursive: true, force: true })
96
+ })
97
+
98
+ describe('primitive tools', () => {
99
+ it('document tool reads, stores, and searches extracted text', async () => {
100
+ const sourcePath = path.join(workspaceDir, 'note.txt')
101
+ fs.writeFileSync(sourcePath, 'Invoice 42 for ACME\nTotal: $120.50\n')
102
+
103
+ const [documentTool] = buildDocumentTools(makeBuildContext())
104
+ const read = JSON.parse(String(await documentTool.invoke({ action: 'read', filePath: 'note.txt' })))
105
+ assert.match(read.text, /Invoice 42/)
106
+
107
+ const stored = JSON.parse(String(await documentTool.invoke({ action: 'store', filePath: 'note.txt', title: 'Invoice Note' })))
108
+ const search = JSON.parse(String(await documentTool.invoke({ action: 'search', query: 'ACME' })))
109
+ const fetched = JSON.parse(String(await documentTool.invoke({ action: 'get', id: stored.id })))
110
+
111
+ assert.equal(search.matches[0].id, stored.id)
112
+ assert.equal(fetched.title, 'Invoice Note')
113
+ })
114
+
115
+ it('table tool transforms inline data and writes results', async () => {
116
+ const [tableTool] = buildTableTools(makeBuildContext())
117
+ const rows = [
118
+ { id: '1', name: 'Ada', score: 10 },
119
+ { id: '2', name: 'Grace', score: 25 },
120
+ { id: '2', name: 'Grace', score: 25 },
121
+ ]
122
+
123
+ const filtered = JSON.parse(String(await tableTool.invoke({
124
+ action: 'filter',
125
+ rows,
126
+ where: [{ column: 'score', op: 'gt', value: 15 }],
127
+ })))
128
+ assert.equal(filtered.rowCount, 2)
129
+
130
+ const deduped = JSON.parse(String(await tableTool.invoke({
131
+ action: 'dedupe',
132
+ rows,
133
+ on: ['id'],
134
+ })))
135
+ assert.equal(deduped.rowCount, 2)
136
+
137
+ const joined = JSON.parse(String(await tableTool.invoke({
138
+ action: 'join',
139
+ leftRows: [{ id: '1', team: 'red' }],
140
+ rightRows: [{ id: '1', email: 'ada@example.com' }],
141
+ on: 'id',
142
+ })))
143
+ assert.equal(joined.rows[0].email, 'ada@example.com')
144
+
145
+ const writeResult = JSON.parse(String(await tableTool.invoke({
146
+ action: 'write',
147
+ rows,
148
+ outputPath: 'exports/report.csv',
149
+ })))
150
+ assert.equal(fs.existsSync(writeResult.output.filePath), true)
151
+ })
152
+
153
+ it('human-loop tool creates durable mailbox and approval waits', async () => {
154
+ const [humanTool] = buildHumanLoopTools(makeBuildContext())
155
+ const sessions = storage.loadSessions()
156
+ sessions.session_1 = makeSession({ id: 'session_1', agentId: 'agent_1' })
157
+ storage.saveSessions(sessions)
158
+
159
+ const requestInput = JSON.parse(String(await humanTool.invoke({
160
+ action: 'request_input',
161
+ question: 'Ship it?',
162
+ correlationId: 'corr_123',
163
+ })))
164
+ assert.equal(requestInput.ok, true)
165
+
166
+ const replyWatch = JSON.parse(String(await humanTool.invoke({
167
+ action: 'wait_for_reply',
168
+ correlationId: 'corr_123',
169
+ })))
170
+ const replyEnvelope = sessionMailbox.sendMailboxEnvelope({
171
+ toSessionId: 'session_1',
172
+ type: 'human_reply',
173
+ payload: 'yes',
174
+ correlationId: 'corr_123',
175
+ })
176
+ watchJobs.triggerMailboxWatchJobs({ sessionId: 'session_1', envelope: replyEnvelope })
177
+ assert.equal(watchJobs.getWatchJob(replyWatch.id)?.status, 'triggered')
178
+
179
+ const approval = JSON.parse(String(await humanTool.invoke({
180
+ action: 'request_approval',
181
+ title: 'Need signoff',
182
+ question: 'Allow publish?',
183
+ })))
184
+ const approvalWatch = JSON.parse(String(await humanTool.invoke({
185
+ action: 'wait_for_approval',
186
+ approvalId: approval.id,
187
+ })))
188
+ watchJobs.triggerApprovalWatchJobs({ approvalId: approval.id, status: 'approved' })
189
+ assert.equal(watchJobs.getWatchJob(approvalWatch.id)?.status, 'triggered')
190
+ })
191
+
192
+ it('mailbox tool reports configuration status without requiring network', async () => {
193
+ const [mailboxTool] = buildMailboxTools(makeBuildContext())
194
+ const status = JSON.parse(String(await mailboxTool.invoke({ action: 'status' })))
195
+ assert.equal(status.configured, false)
196
+ assert.equal(status.folder, 'INBOX')
197
+ })
198
+
199
+ it('extract tool reports active model context', async () => {
200
+ const [extractTool] = buildExtractTools(makeBuildContext({
201
+ session: makeSession({
202
+ provider: 'ollama',
203
+ model: 'qwen3.5',
204
+ apiEndpoint: 'http://localhost:11434',
205
+ }),
206
+ }))
207
+ const status = JSON.parse(String(await extractTool.invoke({ action: 'status' })))
208
+ assert.equal(status.provider, 'ollama')
209
+ assert.equal(Array.isArray(status.supports), true)
210
+ })
211
+
212
+ it('crawl tool crawls and dedupes fetched pages without a live server', async () => {
213
+ const originalFetch = global.fetch
214
+ global.fetch = (async (input: RequestInfo | URL) => {
215
+ const url = typeof input === 'string' ? input : input.toString()
216
+ if (url.endsWith('/page-2')) {
217
+ return new Response('<html><head><title>Page Two</title></head><body><article><h1>Second</h1><p>Next content</p></article></body></html>', {
218
+ status: 200,
219
+ headers: { 'content-type': 'text/html' },
220
+ })
221
+ }
222
+ return new Response('<html><head><title>Root</title></head><body><article><h1>Home</h1><p>Welcome</p></article><a href="/page-2" rel="next">Next</a></body></html>', {
223
+ status: 200,
224
+ headers: { 'content-type': 'text/html' },
225
+ })
226
+ }) as typeof fetch
227
+
228
+ try {
229
+ const [crawlTool] = buildCrawlTools(makeBuildContext())
230
+ const baseUrl = 'https://example.test/'
231
+
232
+ const crawled = JSON.parse(String(await crawlTool.invoke({
233
+ action: 'crawl_site',
234
+ url: baseUrl,
235
+ limit: 2,
236
+ })))
237
+ assert.equal(crawled.count, 2)
238
+ assert.equal(crawled.pages[0].title, 'Root')
239
+
240
+ const extracted = JSON.parse(String(await crawlTool.invoke({
241
+ action: 'extract_sitemap',
242
+ url: baseUrl,
243
+ limit: 2,
244
+ })))
245
+ assert.equal(extracted.count, 2)
246
+ assert.equal(extracted.urls.includes('https://example.test/page-2'), true)
247
+
248
+ const deduped = JSON.parse(String(await crawlTool.invoke({
249
+ action: 'dedupe_pages',
250
+ pages: [crawled.pages[0], crawled.pages[0], crawled.pages[1]],
251
+ })))
252
+ assert.equal(deduped.count, 2)
253
+ } finally {
254
+ global.fetch = originalFetch
255
+ }
256
+ })
257
+ })