@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.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /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 {
|
|
44
|
-
|
|
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.
|
|
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.
|
|
84
|
+
const currentTools = session.plugins || []
|
|
74
85
|
if (!currentTools.includes(filename)) {
|
|
75
|
-
session.
|
|
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
|
|
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 ({
|
|
104
|
-
afterToolExec: async ({ session, toolName,
|
|
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
|
|
144
|
-
- SwarmClaw checks
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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
|
+
})
|