@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
@@ -0,0 +1,380 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import type { Plugin, PluginHooks } from '@/types'
6
+ import { getPluginManager } from '../plugins'
7
+ import { normalizeToolInputArgs } from './normalize-tool-args'
8
+ import { UPLOAD_DIR } from '../storage'
9
+ import type { ToolBuildContext } from './context'
10
+
11
+ type ImageProvider = 'openai' | 'stability' | 'replicate' | 'fal' | 'together' | 'fireworks' | 'bfl' | 'custom'
12
+
13
+ interface PluginConfig {
14
+ provider: ImageProvider
15
+ apiKey: string
16
+ model: string
17
+ defaultSize: string
18
+ customEndpoint: string
19
+ }
20
+
21
+ function getConfig(): PluginConfig {
22
+ const ps = getPluginManager().getPluginSettings('image_gen')
23
+ return {
24
+ provider: (ps.provider as ImageProvider) || 'openai',
25
+ apiKey: (ps.apiKey as string) || '',
26
+ model: (ps.model as string) || '',
27
+ defaultSize: (ps.defaultSize as string) || '1024x1024',
28
+ customEndpoint: (ps.customEndpoint as string) || '',
29
+ }
30
+ }
31
+
32
+ type GenResult = { b64?: string; url?: string; error?: string }
33
+
34
+ // --- Provider Implementations ---
35
+
36
+ async function generateOpenAI(prompt: string, size: string, quality: string, cfg: PluginConfig): Promise<GenResult> {
37
+ const model = cfg.model || 'gpt-image-1'
38
+ const res = await fetch('https://api.openai.com/v1/images/generations', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}` },
41
+ body: JSON.stringify({ model, prompt, n: 1, size, quality, response_format: 'b64_json' }),
42
+ signal: AbortSignal.timeout(120_000),
43
+ })
44
+ if (!res.ok) return { error: `OpenAI ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
45
+ const data = await res.json()
46
+ return { b64: data?.data?.[0]?.b64_json }
47
+ }
48
+
49
+ async function generateStability(prompt: string, size: string, cfg: PluginConfig): Promise<GenResult> {
50
+ // Stability v2beta uses multipart/form-data and returns raw image bytes
51
+ const model = cfg.model || 'sd3'
52
+ const formData = new FormData()
53
+ formData.append('prompt', prompt)
54
+ formData.append('model', model)
55
+ formData.append('output_format', 'png')
56
+ // Map size to aspect ratio
57
+ const [w, h] = size.split('x').map(Number)
58
+ if (w && h) {
59
+ const ratio = w > h ? '16:9' : h > w ? '9:16' : '1:1'
60
+ formData.append('aspect_ratio', ratio)
61
+ }
62
+ const res = await fetch('https://api.stability.ai/v2beta/stable-image/generate/sd3', {
63
+ method: 'POST',
64
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, Accept: 'image/*' },
65
+ body: formData,
66
+ signal: AbortSignal.timeout(120_000),
67
+ })
68
+ if (!res.ok) return { error: `Stability ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
69
+ const buf = Buffer.from(await res.arrayBuffer())
70
+ return { b64: buf.toString('base64') }
71
+ }
72
+
73
+ async function generateReplicate(prompt: string, size: string, cfg: PluginConfig): Promise<GenResult> {
74
+ const model = cfg.model || 'black-forest-labs/flux-schnell'
75
+ const [w, h] = size.split('x').map(Number)
76
+ // Try sync mode first (Prefer: wait blocks up to 60s)
77
+ const createRes = await fetch('https://api.replicate.com/v1/predictions', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}`, Prefer: 'wait' },
80
+ body: JSON.stringify({ model, input: { prompt, width: w || 1024, height: h || 1024 } }),
81
+ signal: AbortSignal.timeout(120_000),
82
+ })
83
+ if (!createRes.ok) return { error: `Replicate ${createRes.status}: ${(await createRes.text().catch(() => '')).slice(0, 300)}` }
84
+ let prediction = await createRes.json()
85
+
86
+ // If sync didn't complete, poll
87
+ if (prediction.status !== 'succeeded' && prediction.status !== 'failed' && prediction.urls?.get) {
88
+ const deadline = Date.now() + 120_000
89
+ while (prediction.status !== 'succeeded' && prediction.status !== 'failed' && Date.now() < deadline) {
90
+ await new Promise((r) => setTimeout(r, 2000))
91
+ const pollRes = await fetch(prediction.urls.get, {
92
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
93
+ signal: AbortSignal.timeout(10_000),
94
+ })
95
+ prediction = await pollRes.json()
96
+ }
97
+ }
98
+ if (prediction.status === 'failed') return { error: `Replicate failed: ${prediction.error || 'unknown'}` }
99
+ const output = Array.isArray(prediction.output) ? prediction.output[0] : prediction.output
100
+ if (typeof output === 'string') return { url: output }
101
+ return { error: 'No image in Replicate response.' }
102
+ }
103
+
104
+ async function generateFal(prompt: string, size: string, cfg: PluginConfig): Promise<GenResult> {
105
+ const model = cfg.model || 'fal-ai/flux/schnell'
106
+ const [w, h] = size.split('x').map(Number)
107
+ const res = await fetch(`https://fal.run/${model}`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json', Authorization: `Key ${cfg.apiKey}` },
110
+ body: JSON.stringify({ prompt, image_size: { width: w || 1024, height: h || 1024 }, num_images: 1 }),
111
+ signal: AbortSignal.timeout(120_000),
112
+ })
113
+ if (!res.ok) return { error: `fal.ai ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
114
+ const data = await res.json()
115
+ const imageUrl = data?.images?.[0]?.url
116
+ if (imageUrl) return { url: imageUrl }
117
+ return { error: 'No image in fal.ai response.' }
118
+ }
119
+
120
+ async function generateTogether(prompt: string, size: string, cfg: PluginConfig): Promise<GenResult> {
121
+ const model = cfg.model || 'black-forest-labs/FLUX.1-schnell-Free'
122
+ const [w, h] = size.split('x').map(Number)
123
+ const res = await fetch('https://api.together.xyz/v1/images/generations', {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}` },
126
+ body: JSON.stringify({ model, prompt, width: w || 1024, height: h || 1024, n: 1, response_format: 'b64_json' }),
127
+ signal: AbortSignal.timeout(120_000),
128
+ })
129
+ if (!res.ok) return { error: `Together ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
130
+ const data = await res.json()
131
+ const b64 = data?.data?.[0]?.b64_json
132
+ if (b64) return { b64 }
133
+ const url = data?.data?.[0]?.url
134
+ if (url) return { url }
135
+ return { error: 'No image in Together response.' }
136
+ }
137
+
138
+ async function generateFireworks(prompt: string, _size: string, cfg: PluginConfig): Promise<GenResult> {
139
+ const model = cfg.model || 'flux-1-schnell-fp8'
140
+ const res = await fetch(`https://api.fireworks.ai/inference/v1/workflows/accounts/fireworks/models/${model}/text_to_image`, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}`, Accept: 'image/jpeg' },
143
+ body: JSON.stringify({ prompt }),
144
+ signal: AbortSignal.timeout(120_000),
145
+ })
146
+ if (!res.ok) return { error: `Fireworks ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
147
+ // Response may be JSON with base64 array or raw image
148
+ const ct = res.headers.get('content-type') || ''
149
+ if (ct.includes('application/json')) {
150
+ const data = await res.json()
151
+ const b64 = data?.base64?.[0] ?? data?.data?.[0]?.b64_json
152
+ if (b64) return { b64 }
153
+ return { error: 'No image in Fireworks JSON response.' }
154
+ }
155
+ // Raw image bytes
156
+ const buf = Buffer.from(await res.arrayBuffer())
157
+ return { b64: buf.toString('base64') }
158
+ }
159
+
160
+ async function generateBFL(prompt: string, size: string, cfg: PluginConfig): Promise<GenResult> {
161
+ const [w, h] = size.split('x').map(Number)
162
+ const model = cfg.model || 'flux-pro-1.1'
163
+ const createRes = await fetch(`https://api.bfl.ai/v1/${model}`, {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json', 'x-key': cfg.apiKey },
166
+ body: JSON.stringify({ prompt, width: w || 1024, height: h || 1024 }),
167
+ signal: AbortSignal.timeout(15_000),
168
+ })
169
+ if (!createRes.ok) return { error: `BFL ${createRes.status}: ${(await createRes.text().catch(() => '')).slice(0, 300)}` }
170
+ const task = await createRes.json()
171
+ const pollingUrl = task?.polling_url || (task?.id ? `https://api.bfl.ai/v1/get_result?id=${task.id}` : null)
172
+ if (!pollingUrl) return { error: 'No polling URL from BFL.' }
173
+
174
+ // Poll for result
175
+ const deadline = Date.now() + 120_000
176
+ while (Date.now() < deadline) {
177
+ await new Promise((r) => setTimeout(r, 2000))
178
+ const pollRes = await fetch(pollingUrl, {
179
+ headers: { 'x-key': cfg.apiKey },
180
+ signal: AbortSignal.timeout(10_000),
181
+ })
182
+ const result = await pollRes.json()
183
+ if (result.status === 'Ready' && result.result?.sample) return { url: result.result.sample }
184
+ if (result.status === 'Error') return { error: `BFL error: ${result.result || 'unknown'}` }
185
+ }
186
+ return { error: 'BFL generation timed out.' }
187
+ }
188
+
189
+ async function generateCustom(prompt: string, size: string, quality: string, cfg: PluginConfig): Promise<GenResult> {
190
+ if (!cfg.customEndpoint) return { error: 'Custom endpoint URL not configured.' }
191
+ // Assumes OpenAI-compatible image generation API
192
+ const [w, h] = size.split('x').map(Number)
193
+ const res = await fetch(cfg.customEndpoint, {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}` },
196
+ body: JSON.stringify({ model: cfg.model || 'default', prompt, n: 1, size, width: w || 1024, height: h || 1024, quality, response_format: 'b64_json' }),
197
+ signal: AbortSignal.timeout(120_000),
198
+ })
199
+ if (!res.ok) return { error: `Custom API ${res.status}: ${(await res.text().catch(() => '')).slice(0, 300)}` }
200
+ const data = await res.json()
201
+ const b64 = data?.data?.[0]?.b64_json ?? data?.images?.[0]?.b64_json ?? data?.b64_json ?? data?.artifacts?.[0]?.base64
202
+ if (b64) return { b64 }
203
+ const url = data?.data?.[0]?.url ?? data?.images?.[0]?.url ?? data?.url
204
+ if (url) return { url }
205
+ return { error: 'No image found in custom API response.' }
206
+ }
207
+
208
+ // --- Dispatcher ---
209
+
210
+ const PROVIDERS: Record<ImageProvider, (prompt: string, size: string, quality: string, cfg: PluginConfig) => Promise<GenResult>> = {
211
+ openai: generateOpenAI,
212
+ stability: (p, s, _q, c) => generateStability(p, s, c),
213
+ replicate: (p, s, _q, c) => generateReplicate(p, s, c),
214
+ fal: (p, s, _q, c) => generateFal(p, s, c),
215
+ together: (p, s, _q, c) => generateTogether(p, s, c),
216
+ fireworks: (p, s, _q, c) => generateFireworks(p, s, c),
217
+ bfl: (p, s, _q, c) => generateBFL(p, s, c),
218
+ custom: generateCustom,
219
+ }
220
+
221
+ async function saveImageResult(result: GenResult, prompt: string, filename: string | undefined): Promise<string> {
222
+ if (result.error) return `Error: ${result.error}`
223
+
224
+ if (result.b64) {
225
+ const buf = Buffer.from(result.b64, 'base64')
226
+ const baseName = filename || `img-${Date.now()}.png`
227
+ const safeName = baseName.replace(/[^a-zA-Z0-9._-]/g, '_')
228
+ const dest = path.join(UPLOAD_DIR, safeName)
229
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
230
+ fs.writeFileSync(dest, buf)
231
+ return `Image generated (${buf.length} bytes).\n\n![${prompt.slice(0, 60)}](/api/uploads/${safeName})\n\n[Download](/api/uploads/${safeName})`
232
+ }
233
+
234
+ if (result.url) {
235
+ // Download remote URL to uploads
236
+ try {
237
+ const res = await fetch(result.url, { signal: AbortSignal.timeout(30_000) })
238
+ if (res.ok) {
239
+ const buf = Buffer.from(await res.arrayBuffer())
240
+ const baseName = filename || `img-${Date.now()}.png`
241
+ const safeName = baseName.replace(/[^a-zA-Z0-9._-]/g, '_')
242
+ const dest = path.join(UPLOAD_DIR, safeName)
243
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
244
+ fs.writeFileSync(dest, buf)
245
+ return `Image generated (${buf.length} bytes).\n\n![${prompt.slice(0, 60)}](/api/uploads/${safeName})\n\n[Download](/api/uploads/${safeName})`
246
+ }
247
+ } catch { /* fall through to URL-only response */ }
248
+ return `Image generated: ${result.url}`
249
+ }
250
+
251
+ return 'Error: No image returned.'
252
+ }
253
+
254
+ async function executeImageGen(args: Record<string, unknown>): Promise<string> {
255
+ const normalized = normalizeToolInputArgs(args)
256
+ const prompt = String(normalized.prompt || '').trim()
257
+ if (!prompt) return 'Error: prompt is required.'
258
+
259
+ const cfg = getConfig()
260
+ if (!cfg.apiKey) return 'Error: Image generation API key not configured. Ask the user to add one in Plugin Settings > Image Generation.'
261
+
262
+ const size = String(normalized.size || cfg.defaultSize)
263
+ const quality = String(normalized.quality || 'standard')
264
+ const filename = normalized.filename as string | undefined
265
+
266
+ const generate = PROVIDERS[cfg.provider]
267
+ if (!generate) return `Error: Unknown provider "${cfg.provider}".`
268
+
269
+ try {
270
+ const result = await generate(prompt, size, quality, cfg)
271
+ return saveImageResult(result, prompt, filename)
272
+ } catch (err: unknown) {
273
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
274
+ }
275
+ }
276
+
277
+ const ImageGenPlugin: Plugin = {
278
+ name: 'Image Generation',
279
+ enabledByDefault: false,
280
+ description: 'Generate images from text prompts. Supports OpenAI, Stability AI, Replicate, fal.ai, Together AI, Fireworks AI, BFL (Flux), or any OpenAI-compatible API.',
281
+ hooks: {
282
+ getCapabilityDescription: () =>
283
+ 'I can generate images from text descriptions using `generate_image`. Supports different sizes, quality levels, and providers.',
284
+ } as PluginHooks,
285
+ tools: [
286
+ {
287
+ name: 'generate_image',
288
+ description: 'Generate an image from a text prompt. The image is saved and a download link is returned. Use descriptive, detailed prompts for best results.',
289
+ parameters: {
290
+ type: 'object',
291
+ properties: {
292
+ prompt: { type: 'string', description: 'Detailed text description of the image to generate' },
293
+ size: { type: 'string', enum: ['1024x1024', '1536x1024', '1024x1536', '512x512', '768x768', '1280x720', '720x1280'], description: 'Image dimensions (default: 1024x1024)' },
294
+ quality: { type: 'string', enum: ['standard', 'hd', 'low', 'medium', 'high'], description: 'Quality level (default: standard). Primarily used by OpenAI.' },
295
+ filename: { type: 'string', description: 'Optional filename for the saved image (e.g. "hero-banner.png")' },
296
+ },
297
+ required: ['prompt'],
298
+ },
299
+ execute: async (args) => executeImageGen(args),
300
+ },
301
+ ],
302
+ ui: {
303
+ settingsFields: [
304
+ {
305
+ key: 'provider',
306
+ label: 'Provider',
307
+ type: 'select',
308
+ options: [
309
+ { value: 'openai', label: 'OpenAI (DALL-E / gpt-image)' },
310
+ { value: 'stability', label: 'Stability AI' },
311
+ { value: 'replicate', label: 'Replicate (Flux, SDXL, etc.)' },
312
+ { value: 'fal', label: 'fal.ai (Flux, SDXL, etc.)' },
313
+ { value: 'together', label: 'Together AI' },
314
+ { value: 'fireworks', label: 'Fireworks AI' },
315
+ { value: 'bfl', label: 'BFL / Black Forest Labs (Flux Pro)' },
316
+ { value: 'custom', label: 'Custom (OpenAI-compatible endpoint)' },
317
+ ],
318
+ defaultValue: 'openai',
319
+ },
320
+ {
321
+ key: 'apiKey',
322
+ label: 'API Key',
323
+ type: 'secret',
324
+ required: true,
325
+ placeholder: 'sk-... / r8_... / fal-...',
326
+ help: 'API key for the selected provider.',
327
+ },
328
+ {
329
+ key: 'model',
330
+ label: 'Model',
331
+ type: 'text',
332
+ placeholder: 'gpt-image-1 / black-forest-labs/flux-schnell / ...',
333
+ help: 'Model ID. Each provider has its own default if left blank.',
334
+ },
335
+ {
336
+ key: 'defaultSize',
337
+ label: 'Default Size',
338
+ type: 'select',
339
+ options: [
340
+ { value: '1024x1024', label: '1024x1024 (Square)' },
341
+ { value: '1536x1024', label: '1536x1024 (Landscape)' },
342
+ { value: '1024x1536', label: '1024x1536 (Portrait)' },
343
+ { value: '1280x720', label: '1280x720 (16:9)' },
344
+ { value: '512x512', label: '512x512 (Small)' },
345
+ { value: '768x768', label: '768x768 (Medium)' },
346
+ ],
347
+ defaultValue: '1024x1024',
348
+ },
349
+ {
350
+ key: 'customEndpoint',
351
+ label: 'Custom Endpoint URL',
352
+ type: 'text',
353
+ placeholder: 'https://your-api.example.com/v1/images/generations',
354
+ help: 'Only used when provider is "Custom". Should accept OpenAI-compatible image generation requests.',
355
+ },
356
+ ],
357
+ },
358
+ }
359
+
360
+ getPluginManager().registerBuiltin('image_gen', ImageGenPlugin)
361
+
362
+ export function buildImageGenTools(bctx: ToolBuildContext): StructuredToolInterface[] {
363
+ if (!bctx.hasPlugin('image_gen')) return []
364
+
365
+ return [
366
+ tool(
367
+ async (args) => executeImageGen(args),
368
+ {
369
+ name: 'generate_image',
370
+ description: ImageGenPlugin.tools![0].description,
371
+ schema: z.object({
372
+ prompt: z.string().describe('Detailed text description of the image to generate'),
373
+ size: z.enum(['1024x1024', '1536x1024', '1024x1536', '512x512', '768x768', '1280x720', '720x1280']).optional().describe('Image dimensions (default: 1024x1024)'),
374
+ quality: z.enum(['standard', 'hd', 'low', 'medium', 'high']).optional().describe('Quality level (default: standard)'),
375
+ filename: z.string().optional().describe('Optional filename for the saved image'),
376
+ }),
377
+ },
378
+ ),
379
+ ]
380
+ }
@@ -5,7 +5,7 @@ import { loadSettings, loadSessions, saveSessions, loadMcpServers } from '../sto
5
5
  import { loadRuntimeSettings } from '../runtime-settings'
6
6
  import { log } from '../logger'
7
7
  import { resolveSessionToolPolicy } from '../tool-capability-policy'
8
- import { expandToolIds } from '../tool-aliases'
8
+ import { expandPluginIds } from '../tool-aliases'
9
9
  import type { ToolContext, SessionToolsResult, ToolBuildContext } from './context'
10
10
 
11
11
  // Import all tool modules to trigger their builtin registration
@@ -33,6 +33,16 @@ import { buildDiscoveryTools } from './discovery'
33
33
  import { buildMonitorTools } from './monitor'
34
34
  import { buildSampleUITools } from './sample-ui'
35
35
  import { buildPluginCreatorTools } from './plugin-creator'
36
+ import { buildImageGenTools } from './image-gen'
37
+ import { buildEmailTools } from './email'
38
+ import { buildCalendarTools } from './calendar'
39
+ import { buildReplicateTools } from './replicate'
40
+ import { buildMailboxTools } from './mailbox'
41
+ import { buildHumanLoopTools } from './human-loop'
42
+ import { buildDocumentTools } from './document'
43
+ import { buildExtractTools } from './extract'
44
+ import { buildTableTools } from './table'
45
+ import { buildCrawlTools } from './crawl'
36
46
  import { normalizeToolInputArgs } from './normalize-tool-args'
37
47
 
38
48
  import { getPluginManager } from '../plugins'
@@ -41,34 +51,37 @@ import { jsonSchemaToZod } from '../mcp-client'
41
51
  export type { ToolContext, SessionToolsResult }
42
52
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
43
53
 
44
- export async function buildSessionTools(cwd: string, enabledTools: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
54
+ export async function buildSessionTools(cwd: string, enabledPlugins: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
45
55
  const tools: StructuredToolInterface[] = []
46
56
  const cleanupFns: (() => Promise<void>)[] = []
47
-
57
+
48
58
  try {
49
59
  const runtime = loadRuntimeSettings()
50
60
  const commandTimeoutMs = runtime.shellCommandTimeoutMs
51
61
  const claudeTimeoutMs = runtime.claudeCodeTimeoutMs
52
62
  const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
53
63
  const appSettings = loadSettings()
54
- const toolPolicy = resolveSessionToolPolicy(enabledTools, appSettings)
55
- const expandedEnabledTools = expandToolIds(toolPolicy.enabledTools)
56
- const expandedBlockedTools = expandToolIds(toolPolicy.blockedTools.map((entry) => entry.tool))
57
- const blockedToolSet = new Set(expandedBlockedTools)
58
- const filteredEnabledTools = expandedEnabledTools.filter((toolId) => !blockedToolSet.has(toolId))
59
- const activeTools = filteredEnabledTools.includes('shell')
60
- && !filteredEnabledTools.includes('process')
61
- && !blockedToolSet.has('process')
62
- ? [...filteredEnabledTools, 'process']
63
- : filteredEnabledTools
64
- const activeToolSet = new Set(activeTools)
65
- const hasTool = (toolName: string) => activeToolSet.has(toolName)
64
+ const toolPolicy = resolveSessionToolPolicy(enabledPlugins, appSettings)
65
+ const expandedEnabled = expandPluginIds(toolPolicy.enabledPlugins)
66
+ const expandedBlocked = expandPluginIds(toolPolicy.blockedPlugins.map((entry) => entry.tool))
67
+ const blockedSet = new Set(expandedBlocked)
68
+ const filteredEnabled = expandedEnabled.filter((id) => !blockedSet.has(id))
69
+ const pluginManager = getPluginManager()
70
+ const activePlugins = (filteredEnabled.includes('shell')
71
+ && !filteredEnabled.includes('process')
72
+ && !blockedSet.has('process')
73
+ ? [...filteredEnabled, 'process']
74
+ : filteredEnabled).filter(t => pluginManager.isEnabled(t))
75
+ const activePluginSet = new Set(activePlugins)
76
+ const hasPlugin = (pluginName: string) => activePluginSet.has(pluginName)
77
+ /** @deprecated Use hasPlugin */
78
+ const hasTool = hasPlugin
66
79
 
67
- if (toolPolicy.blockedTools.length > 0) {
68
- log.info('session-tools', 'Capability policy blocked tool families', {
80
+ if (toolPolicy.blockedPlugins.length > 0) {
81
+ log.info('session-tools', 'Capability policy blocked plugin families', {
69
82
  sessionId: ctx?.sessionId || null,
70
83
  agentId: ctx?.agentId || null,
71
- blockedTools: toolPolicy.blockedTools.map((entry) => `${entry.tool}:${entry.reason}`),
84
+ blockedPlugins: toolPolicy.blockedPlugins.map((entry) => `${entry.tool}:${entry.reason}`),
72
85
  })
73
86
  }
74
87
 
@@ -78,14 +91,14 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
78
91
  return sessions[ctx.sessionId] || null
79
92
  }
80
93
 
81
- const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode'): string | null => {
94
+ const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini'): string | null => {
82
95
  const session = resolveCurrentSession()
83
96
  if (!session?.delegateResumeIds || typeof session.delegateResumeIds !== 'object') return null
84
97
  const raw = session.delegateResumeIds[key]
85
98
  return typeof raw === 'string' && raw.trim() ? raw.trim() : null
86
99
  }
87
100
 
88
- const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode', resumeId: string | null | undefined): void => {
101
+ const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', resumeId: string | null | undefined): void => {
89
102
  const normalized = typeof resumeId === 'string' ? resumeId.trim() : ''
90
103
  if (!normalized || !ctx?.sessionId) return
91
104
  const sessions = loadSessions()
@@ -106,6 +119,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
106
119
  const bctx: ToolBuildContext = {
107
120
  cwd,
108
121
  ctx,
122
+ hasPlugin,
109
123
  hasTool,
110
124
  cleanupFns,
111
125
  commandTimeoutMs,
@@ -114,41 +128,60 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
114
128
  persistDelegateResumeId,
115
129
  readStoredDelegateResumeId,
116
130
  resolveCurrentSession,
117
- activeTools,
131
+ activePlugins,
118
132
  }
119
133
 
120
134
  // 1. Build Native Bridge Tools (Legacy enablement)
121
- tools.push(
122
- ...buildShellTools(bctx),
123
- ...buildFileTools(bctx),
124
- ...buildEditFileTools(bctx),
125
- ...buildDelegateTools(bctx),
126
- ...buildWebTools(bctx),
127
- ...buildMemoryTools(bctx),
128
- ...buildPlatformTools(bctx),
129
- ...buildSandboxTools(bctx),
130
- ...buildChatroomTools(bctx),
131
- ...buildSubagentTools(bctx),
132
- ...buildCanvasTools(bctx),
133
- ...buildHttpTools(bctx),
134
- ...buildGitTools(bctx),
135
- ...buildWalletTools(bctx),
136
- ...buildOpenClawWorkspaceTools(bctx),
137
- ...buildScheduleTools(bctx),
138
- ...buildSessionInfoTools(bctx),
139
- ...buildOpenClawNodeTools(bctx),
140
- ...buildContextTools(bctx),
141
- ...buildConnectorTools(bctx),
142
- ...buildDiscoveryTools(bctx),
143
- ...buildMonitorTools(bctx),
144
- ...buildSampleUITools(bctx),
145
- ...buildPluginCreatorTools(bctx),
146
- )
135
+ const toolToPluginMap: Record<string, string> = {}
136
+
137
+ const nativeBuilders: Array<[string, (ctx: ToolBuildContext) => StructuredToolInterface[]]> = [
138
+ ['shell', buildShellTools],
139
+ ['files', buildFileTools],
140
+ ['edit_file', buildEditFileTools],
141
+ ['delegate', buildDelegateTools],
142
+ ['web', buildWebTools],
143
+ ['memory', buildMemoryTools],
144
+ ['manage_platform', buildPlatformTools],
145
+ ['sandbox', buildSandboxTools],
146
+ ['manage_chatrooms', buildChatroomTools],
147
+ ['spawn_subagent', buildSubagentTools],
148
+ ['canvas', buildCanvasTools],
149
+ ['http', buildHttpTools],
150
+ ['git', buildGitTools],
151
+ ['wallet', buildWalletTools],
152
+ ['openclaw_workspace', buildOpenClawWorkspaceTools],
153
+ ['schedule', buildScheduleTools],
154
+ ['manage_sessions', buildSessionInfoTools],
155
+ ['openclaw_nodes', buildOpenClawNodeTools],
156
+ ['context_mgmt', buildContextTools],
157
+ ['manage_connectors', buildConnectorTools],
158
+ ['discovery', buildDiscoveryTools],
159
+ ['monitor', buildMonitorTools],
160
+ ['sample_ui', buildSampleUITools],
161
+ ['plugin_creator', buildPluginCreatorTools],
162
+ ['image_gen', buildImageGenTools],
163
+ ['email', buildEmailTools],
164
+ ['calendar', buildCalendarTools],
165
+ ['replicate', buildReplicateTools],
166
+ ['mailbox', buildMailboxTools],
167
+ ['ask_human', buildHumanLoopTools],
168
+ ['document', buildDocumentTools],
169
+ ['extract', buildExtractTools],
170
+ ['table', buildTableTools],
171
+ ['crawl', buildCrawlTools],
172
+ ]
173
+
174
+ for (const [pluginId, builder] of nativeBuilders) {
175
+ const builtTools = builder(bctx)
176
+ for (const t of builtTools) {
177
+ toolToPluginMap[t.name] = pluginId
178
+ }
179
+ tools.push(...builtTools)
180
+ }
147
181
 
148
182
  // 2. Build Plugin Tools (Built-in + External)
149
183
  try {
150
- const pluginManager = getPluginManager()
151
- const pluginTools = pluginManager.getTools(activeTools)
184
+ const pluginTools = pluginManager.getTools(activePlugins)
152
185
  const existingNames = new Set(tools.map((t) => t.name))
153
186
 
154
187
  for (const entry of pluginTools) {
@@ -161,6 +194,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
161
194
  continue
162
195
  }
163
196
  existingNames.add(pt.name)
197
+ toolToPluginMap[pt.name] = entry.pluginId
164
198
 
165
199
  tools.push(
166
200
  tool(
@@ -208,6 +242,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
208
242
  const mcpLcTools = await mcpToolsToLangChain(conn.client, config.name)
209
243
  for (const t of mcpLcTools) {
210
244
  if (!disabledMcpToolNames.has(t.name)) {
245
+ toolToPluginMap[t.name] = `mcp:${serverId}`
211
246
  tools.push(t)
212
247
  }
213
248
  }
@@ -224,6 +259,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
224
259
  }
225
260
 
226
261
  // 4. Always available: request_tool_access
262
+ toolToPluginMap['request_tool_access'] = '_system'
227
263
  tools.push(
228
264
  tool(
229
265
  async (args) => {
@@ -248,13 +284,57 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
248
284
  ),
249
285
  )
250
286
 
287
+ const buildFallbackHookSession = (): Session => ({
288
+ id: ctx?.sessionId || 'plugin-hook-session',
289
+ name: 'Plugin Hook Session',
290
+ cwd,
291
+ user: 'system',
292
+ provider: 'openai',
293
+ model: 'unknown',
294
+ claudeSessionId: null,
295
+ messages: [],
296
+ createdAt: Date.now(),
297
+ lastActiveAt: Date.now(),
298
+ agentId: ctx?.agentId || null,
299
+ plugins: [...activePlugins],
300
+ })
301
+
302
+ const wrappedTools = tools.map((candidate) => {
303
+ const schema = (candidate as unknown as { schema?: z.ZodTypeAny }).schema || z.object({}).passthrough()
304
+ return tool(
305
+ async (args) => {
306
+ const normalizedArgs = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
307
+ const nextArgs = await pluginManager.runBeforeToolExec(
308
+ { toolName: candidate.name, input: normalizedArgs },
309
+ { enabledIds: activePlugins },
310
+ )
311
+ const effectiveArgs = nextArgs ?? normalizedArgs
312
+ const result = await candidate.invoke(effectiveArgs)
313
+ const outputText = typeof result === 'string' ? result : JSON.stringify(result)
314
+ const hookSession = resolveCurrentSession() || buildFallbackHookSession()
315
+ await pluginManager.runHook(
316
+ 'afterToolExec',
317
+ { session: hookSession, toolName: candidate.name, input: effectiveArgs, output: outputText },
318
+ { enabledIds: activePlugins },
319
+ )
320
+ return outputText
321
+ },
322
+ {
323
+ name: candidate.name,
324
+ description: candidate.description,
325
+ schema,
326
+ },
327
+ )
328
+ })
329
+
251
330
  return {
252
- tools,
331
+ tools: wrappedTools,
253
332
  cleanup: async () => {
254
333
  for (const fn of cleanupFns) {
255
334
  try { await fn() } catch { /* ignore */ }
256
335
  }
257
336
  },
337
+ toolToPluginMap,
258
338
  }
259
339
  } catch (err: any) {
260
340
  console.error('[session-tools] buildSessionTools critical failure:', err.message)